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