]> git.agnieray.net Git - galette.git/blob - galette/lib/Galette/Filters/AdvancedMembersList.php
852d935cd162e2b6738470f0e85531614ce21db7
[galette.git] / galette / lib / Galette / Filters / AdvancedMembersList.php
1 <?php
2
3 /**
4 * Copyright © 2003-2024 The Galette Team
5 *
6 * This file is part of Galette (https://galette.eu).
7 *
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.
12 *
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.
17 *
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/>.
20 */
21
22 namespace Galette\Filters;
23
24 use Throwable;
25 use Analog\Analog;
26 use Galette\Entity\Status;
27 use Galette\Entity\ContributionsTypes;
28 use Galette\Entity\Contribution;
29 use Galette\Repository\Members;
30 use Galette\DynamicFields\DynamicField;
31 use Galette\Repository\PaymentTypes;
32
33 /**
34 * Members list filters and paginator
35 *
36 * @author Johan Cwiklinski <johan@x-tnd.be>
37 *
38 * @property ?string $creation_date_begin
39 * @property ?string $creation_date_end
40 * @property ?string $modif_date_begin
41 * @property ?string $modif_date_end
42 * @property ?string $due_date_begin
43 * @property ?string $due_date_end
44 * @property ?string $birth_date_begin
45 * @property ?string $birth_date_end
46 * @property int $show_public_infos
47 * @property array|integer $status
48 * @property ?string $contrib_creation_date_begin
49 * @property ?string $contrib_creation_date_end
50 * @property ?string $contrib_begin_date_begin
51 * @property ?string $contrib_begin_date_end
52 * @property ?string $contrib_end_date_begin
53 * @property ?string $contrib_end_date_end
54 * @property array $contributions_types
55 * @property array $payments_types
56 * @property ?float $contrib_min_amount
57 * @property ?float $contrib_max_amount
58 * @property array $contrib_dynamic
59 * @property array $free_search
60 * @property array $groups_search
61 * @property integer $groups_search_log_op
62 *
63 * @property-read ?string $rcreation_date_begin
64 * @property-read ?string $rcreation_date_end
65 * @property-read ?string $rmodif_date_begin
66 * @property-read ?string $rmodif_date_end
67 * @property-read ?string $rdue_date_begin
68 * @property-read ?string $rdue_date_end
69 * @property-read ?string $rbirth_date_begin
70 * @property-read ?string $rbirth_date_end
71 * @property-read ?string $rcontrib_creation_date_begin
72 * @property-read ?string $rcontrib_creation_date_end
73 * @property-read ?string $rcontrib_begin_date_begin
74 * @property-read ?string $rcontrib_begin_date_end
75 * @property-read ?string $rcontrib_end_date_begin
76 * @property-read ?string $rcontrib_end_date_end
77 * @property-read array $search_fields
78 */
79
80 class AdvancedMembersList extends MembersList
81 {
82 public const OP_AND = 0;
83 public const OP_OR = 1;
84
85 public const OP_EQUALS = 0;
86 public const OP_CONTAINS = 1;
87 public const OP_NOT_EQUALS = 2;
88 public const OP_NOT_CONTAINS = 3;
89 public const OP_STARTS_WITH = 4;
90 public const OP_ENDS_WITH = 5;
91 public const OP_BEFORE = 6;
92 public const OP_AFTER = 7;
93
94 private ?string $creation_date_begin = null;
95 private ?string $creation_date_end = null;
96 private ?string $modif_date_begin = null;
97 private ?string $modif_date_end = null;
98 private ?string $due_date_begin = null;
99 private ?string $due_date_end = null;
100 private ?string $birth_date_begin = null;
101 private ?string $birth_date_end = null;
102 private int $show_public_infos = Members::FILTER_DC_PUBINFOS;
103 /** @var array<int> */
104 private array $status = array();
105 private ?string $contrib_creation_date_begin = null;
106 private ?string $contrib_creation_date_end = null;
107 private ?string $contrib_begin_date_begin = null;
108 private ?string $contrib_begin_date_end = null;
109 private ?string $contrib_end_date_begin = null;
110 private ?string $contrib_end_date_end = null;
111 /** @var array<int> */
112 private array $contributions_types = array();
113 /** @var array<int> */
114 private array $payments_types = array();
115 private ?float $contrib_min_amount = null;
116 private ?float $contrib_max_amount = null;
117
118 /** @var array<string> */
119 protected array $advancedmemberslist_fields = array(
120 'creation_date_begin',
121 'creation_date_end',
122 'modif_date_begin',
123 'modif_date_end',
124 'due_date_begin',
125 'due_date_end',
126 'birth_date_begin',
127 'birth_date_end',
128 'show_public_infos',
129 'status',
130 'contrib_creation_date_begin',
131 'contrib_creation_date_end',
132 'contrib_begin_date_begin',
133 'contrib_begin_date_end',
134 'contrib_end_date_begin',
135 'contrib_end_date_end',
136 'contributions_types',
137 'payments_types',
138 'contrib_min_amount',
139 'contrib_max_amount',
140 'contrib_dynamic',
141 'free_search',
142 'groups_search',
143 'groups_search_log_op'
144 );
145
146 /** @var array<string> */
147 protected array $virtuals_advancedmemberslist_fields = array(
148 'rcreation_date_begin',
149 'rcreation_date_end',
150 'rmodif_date_begin',
151 'rmodif_date_end',
152 'rdue_date_begin',
153 'rdue_date_end',
154 'rbirth_date_begin',
155 'rbirth_date_end',
156 'rcontrib_creation_date_begin',
157 'rcontrib_creation_date_end',
158 'rcontrib_begin_date_begin',
159 'rcontrib_begin_date_end',
160 'rcontrib_end_date_begin',
161 'rcontrib_end_date_end',
162 'search_fields'
163 );
164
165 /**
166 * an empty free search criteria to begin
167 *
168 * @var array<string,mixed>
169 */
170 private array $free_search = array(
171 'empty' => array(
172 'field' => '',
173 'search' => '',
174 'log_op' => self::OP_AND,
175 'qry_op' => self::OP_EQUALS
176 )
177 );
178
179 /**
180 * an empty group search criteria to begin
181 *
182 * @var array<string,mixed>
183 */
184 private array $groups_search = array(
185 'empty' => array(
186 'group' => '',
187 )
188 );
189
190 //defaults to 'OR' for group search
191 private int $groups_search_log_op = self::OP_OR;
192
193 /**
194 * an empty contributions dynamic field criteria to begin
195 *
196 * @var array<string,mixed>
197 */
198 private array $contrib_dynamic = array();
199
200 /**
201 * Default constructor
202 *
203 * @param ?MembersList $simple A simple filter search to keep
204 */
205 public function __construct(MembersList $simple = null)
206 {
207 parent::__construct();
208 if ($simple instanceof MembersList) {
209 foreach ($this->pagination_fields as $pf) {
210 $this->$pf = $simple->$pf;
211 }
212 foreach ($this->memberslist_fields as $mlf) {
213 $this->$mlf = $simple->$mlf;
214 }
215 }
216 }
217
218 /**
219 * Do we want to filter within contributions?
220 *
221 * @return boolean
222 */
223 public function withinContributions(): bool
224 {
225 if (
226 $this->contrib_creation_date_begin != null
227 || $this->contrib_creation_date_end != null
228 || $this->contrib_begin_date_begin != null
229 || $this->contrib_begin_date_end != null
230 || $this->contrib_end_date_begin != null
231 || $this->contrib_end_date_end != null
232 || $this->contrib_min_amount != null
233 || $this->contrib_max_amount != null
234 || count($this->contrib_dynamic) > 0
235 || count($this->contributions_types) > 0
236 || count($this->payments_types) > 0
237 ) {
238 return true;
239 } else {
240 return false;
241 }
242 }
243
244 /**
245 * Reinit default parameters
246 *
247 * @return void
248 */
249 public function reinit(): void
250 {
251 parent::reinit();
252
253 $this->creation_date_begin = null;
254 $this->creation_date_end = null;
255 $this->modif_date_begin = null;
256 $this->modif_date_end = null;
257 $this->due_date_begin = null;
258 $this->due_date_end = null;
259 $this->birth_date_begin = null;
260 $this->birth_date_end = null;
261 $this->show_public_infos = Members::FILTER_DC_PUBINFOS;
262 $this->status = array();
263
264 $this->contrib_creation_date_begin = null;
265 $this->contrib_creation_date_end = null;
266 $this->contrib_begin_date_begin = null;
267 $this->contrib_begin_date_end = null;
268 $this->contrib_end_date_begin = null;
269 $this->contrib_begin_date_end = null;
270 $this->contributions_types = array();
271 $this->payments_types = array();
272
273 $this->free_search = array(
274 'empty' => array(
275 'field' => '',
276 'search' => '',
277 'log_op' => self::OP_AND,
278 'qry_op' => self::OP_EQUALS
279 )
280 );
281
282 $this->contrib_dynamic = array();
283
284 $this->groups_search = array(
285 'empty' => array(
286 'group' => '',
287 )
288 );
289
290 $this->groups_search_log_op = self::OP_OR;
291 }
292
293 /**
294 * Global getter method
295 *
296 * @param string $name name of the property we want to retrieve
297 *
298 * @return mixed the called property
299 */
300 public function __get(string $name): mixed
301 {
302 if (
303 in_array($name, $this->pagination_fields)
304 || in_array($name, $this->memberslist_fields)
305 ) {
306 return parent::__get($name);
307 } else {
308 if (
309 in_array($name, $this->advancedmemberslist_fields)
310 || in_array($name, $this->virtuals_advancedmemberslist_fields)
311 ) {
312 switch ($name) {
313 case 'creation_date_begin':
314 case 'creation_date_end':
315 case 'modif_date_begin':
316 case 'modif_date_end':
317 case 'due_date_begin':
318 case 'due_date_end':
319 case 'birth_date_begin':
320 case 'birth_date_end':
321 case 'contrib_creation_date_begin':
322 case 'contrib_creation_date_end':
323 case 'contrib_begin_date_begin':
324 case 'contrib_begin_date_end':
325 case 'contrib_end_date_begin':
326 case 'contrib_end_date_end':
327 try {
328 if ($this->$name !== null) {
329 $d = new \DateTime($this->$name);
330 return $d->format(__("Y-m-d"));
331 }
332 } catch (Throwable $e) {
333 //oops, we've got a bad date :/
334 Analog::log(
335 'Bad date (' . $this->$name . ') | ' .
336 $e->getMessage(),
337 Analog::INFO
338 );
339 return $this->$name;
340 }
341 break;
342 case 'rcreation_date_begin':
343 case 'rcreation_date_end':
344 case 'rmodif_date_begin':
345 case 'rmodif_date_end':
346 case 'rdue_date_begin':
347 case 'rdue_date_end':
348 case 'rbirth_date_begin':
349 case 'rbirth_date_end':
350 case 'rcontrib_creation_date_begin':
351 case 'rcontrib_creation_date_end':
352 case 'rcontrib_begin_date_begin':
353 case 'rcontrib_begin_date_end':
354 case 'rcontrib_end_date_begin':
355 case 'rcontrib_end_date_end':
356 $rname = substr($name, 1);
357 return $this->$rname;
358 case 'search_fields':
359 $search_fields = array_merge($this->memberslist_fields, $this->advancedmemberslist_fields);
360 $key = array_search('selected', $search_fields);
361 unset($search_fields[$key]);
362 $key = array_search('unreachable', $search_fields);
363 unset($search_fields[$key]);
364 $key = array_search('query', $search_fields);
365 unset($search_fields[$key]);
366 return $search_fields;
367 }
368 return $this->$name;
369 }
370 }
371
372 throw new \RuntimeException(
373 sprintf(
374 'Unable to get property "%s::%s"!',
375 __CLASS__,
376 $name
377 )
378 );
379 }
380
381 /**
382 * Global isset method
383 * Required for twig to access properties via __get
384 *
385 * @param string $name name of the property we want to retrieve
386 *
387 * @return bool
388 */
389 public function __isset(string $name): bool
390 {
391 if (
392 in_array($name, $this->pagination_fields)
393 || in_array($name, $this->memberslist_fields)
394 ) {
395 return true;
396 } else {
397 if (
398 in_array($name, $this->advancedmemberslist_fields)
399 || in_array($name, $this->virtuals_advancedmemberslist_fields)
400 ) {
401 return true;
402 }
403 }
404
405 return false;
406 }
407
408 /**
409 * Global setter method
410 *
411 * @param string $name name of the property we want to assign a value to
412 * @param mixed $value a relevant value for the property
413 *
414 * @return void
415 */
416 public function __set(string $name, mixed $value): void
417 {
418 global $zdb, $preferences, $login;
419
420 if (
421 in_array($name, $this->pagination_fields)
422 || in_array($name, $this->memberslist_fields)
423 ) {
424 parent::__set($name, $value);
425 } else {
426 Analog::log(
427 '[AdvancedMembersList] Setting property `' . $name . '`',
428 Analog::DEBUG
429 );
430
431 switch ($name) {
432 case 'creation_date_begin':
433 case 'creation_date_end':
434 case 'modif_date_begin':
435 case 'modif_date_end':
436 case 'due_date_begin':
437 case 'due_date_end':
438 case 'birth_date_begin':
439 case 'birth_date_end':
440 case 'contrib_creation_date_begin':
441 case 'contrib_creation_date_end':
442 case 'contrib_begin_date_begin':
443 case 'contrib_begin_date_end':
444 case 'contrib_end_date_begin':
445 case 'contrib_end_date_end':
446 if ($value !== null && trim($value) !== '') {
447 try {
448 $d = \DateTime::createFromFormat(__("Y-m-d"), $value);
449 if ($d === false) {
450 throw new \Exception('Incorrect format');
451 }
452 $this->$name = $d->format('Y-m-d');
453 } catch (Throwable $e) {
454 Analog::log(
455 'Incorrect date format for ' . $name .
456 '! was: ' . $value,
457 Analog::WARNING
458 );
459 }
460 }
461 break;
462 case 'contrib_min_amount':
463 case 'contrib_max_amount':
464 if (is_float($value)) {
465 $this->$name = $value;
466 } else {
467 if ($value !== null) {
468 Analog::log(
469 'Incorrect amount for ' . $name . '! ' .
470 'Should be a float (' . gettype($value) . ' given)',
471 Analog::WARNING
472 );
473 }
474 }
475 break;
476 case 'show_public_infos':
477 if (is_numeric($value)) {
478 $this->$name = $value;
479 } else {
480 Analog::log(
481 '[AdvancedMembersList] Value for property `' . $name .
482 '` should be an integer (' . gettype($value) . ' given)',
483 Analog::WARNING
484 );
485 }
486 break;
487 case 'status':
488 if (!is_array($value)) {
489 $value = array($value);
490 }
491 $this->status = array();
492 foreach ($value as $v) {
493 if (is_numeric($v)) {
494 //check status existence
495 $s = new Status($zdb);
496 $res = $s->get($v);
497 if ($res !== false) {
498 $this->status[] = $v;
499 } else {
500 Analog::log(
501 'Status #' . $v . ' does not exists!',
502 Analog::WARNING
503 );
504 }
505 } else {
506 Analog::log(
507 '[AdvancedMembersList] Value for status filter should be an '
508 . 'integer (' . gettype($v) . ' given',
509 Analog::WARNING
510 );
511 }
512 }
513 break;
514 case 'contributions_types':
515 if (!is_array($value)) {
516 $value = array($value);
517 }
518 $this->contributions_types = array();
519 foreach ($value as $v) {
520 if (is_numeric($v)) {
521 //check type existence
522 $s = new ContributionsTypes($zdb);
523 $res = $s->get($v);
524 if ($res !== false) {
525 $this->contributions_types[] = $v;
526 } else {
527 Analog::log(
528 'Contribution type #' . $v . ' does not exists!',
529 Analog::WARNING
530 );
531 }
532 } else {
533 Analog::log(
534 '[AdvancedMembersList] Value for contribution type '
535 . 'filter should be an integer (' . gettype($v) .
536 ' given',
537 Analog::WARNING
538 );
539 }
540 }
541 break;
542 case 'payments_types':
543 if (!is_array($value)) {
544 $value = array($value);
545 }
546 $this->payments_types = array();
547 $ptypes = new PaymentTypes(
548 $zdb,
549 $preferences,
550 $login
551 );
552 $ptlist = $ptypes->getList();
553
554 foreach ($value as $v) {
555 if (is_numeric($v)) {
556 if (isset($ptlist[$v])) {
557 $this->payments_types[] = $v;
558 } else {
559 Analog::log(
560 'Payment type #' . $v . ' does not exists!',
561 Analog::WARNING
562 );
563 }
564 } else {
565 Analog::log(
566 '[AdvancedMembersList] Value for payment type filter should be an '
567 . 'integer (' . gettype($v) . ' given',
568 Analog::WARNING
569 );
570 }
571 }
572 break;
573 case 'free_search':
574 if (isset($this->free_search['empty']) && !isset($value['empty'])) {
575 unset($this->free_search['empty']);
576 }
577
578 if ($this->isValidFreeSearch($value)) {
579 //should this happen?
580 $values = [$value];
581 } else {
582 $values = $value;
583 }
584
585 foreach ($values as $value) {
586 if ($this->isValidFreeSearch($value)) {
587 $id = $value['idx'];
588
589 //handle value according to type
590 switch ($value['type']) {
591 case DynamicField::DATE:
592 if ($value['search'] !== null && trim($value['search']) !== '') {
593 try {
594 $d = \DateTime::createFromFormat(__("Y-m-d"), $value['search']);
595 if ($d === false) {
596 throw new \Exception('Incorrect format');
597 }
598 $value['search'] = $d->format('Y-m-d');
599 } catch (Throwable $e) {
600 Analog::log(
601 'Incorrect date format for ' . $value['field'] .
602 '! was: ' . $value['search'],
603 Analog::WARNING
604 );
605 }
606 }
607 break;
608 }
609
610 $this->free_search[$id] = $value;
611 } else {
612 Analog::log(
613 '[AdvancedMembersList] bad construct for free filter',
614 Analog::WARNING
615 );
616 }
617 }
618 break;
619 case 'contrib_dynamic':
620 if (is_array($value)) {
621 $this->contrib_dynamic = $value;
622 } else {
623 Analog::log(
624 '[AdvancedMembersList] Value for dynamic contribution fields filter should be an '
625 . 'array (' . gettype($value) . ' given',
626 Analog::WARNING
627 );
628 }
629 break;
630 case 'groups_search':
631 if (isset($this->groups_search['empty'])) {
632 unset($this->groups_search['empty']);
633 }
634 if (is_array($value)) {
635 if (
636 isset($value['group'])
637 && isset($value['idx'])
638 ) {
639 $id = $value['idx'];
640 unset($value['idx']);
641 $this->groups_search[$id] = $value;
642 } else {
643 Analog::log(
644 '[AdvancedMembersList] bad construct for group filter',
645 Analog::WARNING
646 );
647 }
648 } else {
649 Analog::log(
650 '[AdvancedMembersList] Value for group filter should be an '
651 . 'array (' . gettype($value) . ' given',
652 Analog::WARNING
653 );
654 }
655 break;
656 case 'groups_search_log_op':
657 if ($value == self::OP_AND || $value == self::OP_OR) {
658 $this->groups_search_log_op = $value;
659 } else {
660 Analog::log(
661 '[AdvancedMembersList] Value for group filter logical operator should be '
662 . ' in [0,1] (' . gettype($value) . '-> ' . $value . ' given )',
663 Analog::WARNING
664 );
665 }
666 break;
667 default:
668 if (
669 substr($name, 0, 4) === 'cds_'
670 || substr($name, 0, 5) === 'cdsc_'
671 ) {
672 if (is_array($value) || trim($value) !== '') {
673 $id = null;
674 if (substr($name, 0, 5) === 'cdsc_') {
675 $id = substr($name, 5, strlen($name));
676 } else {
677 $id = substr($name, 4, strlen($name));
678 }
679 $this->contrib_dynamic[$id] = $value;
680 }
681 } else {
682 Analog::log(
683 '[AdvancedMembersList] Unable to set property `' .
684 $name . '`',
685 Analog::WARNING
686 );
687 }
688 break;
689 }
690 }
691 }
692
693 /**
694 * Validate free search internal array
695 *
696 * @param array<string,mixed> $data Array to validate
697 *
698 * @return boolean
699 */
700 public static function isValidFreeSearch(array $data): bool
701 {
702 return isset($data['field'])
703 && isset($data['search'])
704 && isset($data['log_op'])
705 && isset($data['qry_op'])
706 && isset($data['idx'])
707 && isset($data['type']);
708 }
709 }