3 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
6 * Galette contributions controller
10 * Copyright © 2020-2023 The Galette Team
12 * This file is part of Galette (http://galette.tuxfamily.org).
14 * Galette is free software: you can redistribute it and/or modify
15 * it under the terms of the GNU General Public License as published by
16 * the Free Software Foundation, either version 3 of the License, or
17 * (at your option) any later version.
19 * Galette is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU General Public License for more details.
24 * You should have received a copy of the GNU General Public License
25 * along with Galette. If not, see <http://www.gnu.org/licenses/>.
27 * @category Controllers
30 * @author Johan Cwiklinski <johan@x-tnd.be>
31 * @copyright 2020-2023 The Galette Team
32 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
33 * @link http://galette.tuxfamily.org
34 * @since Available since 0.9.4dev - 2020-05-08
37 namespace Galette\Controllers\Crud
;
39 use Galette\Features\BatchList
;
41 use Galette\Controllers\CrudController
;
42 use Slim\Psr7\Request
;
43 use Slim\Psr7\Response
;
44 use Galette\Entity\Adherent
;
45 use Galette\Entity\Contribution
;
46 use Galette\Entity\Transaction
;
47 use Galette\Repository\Members
;
48 use Galette\Entity\ContributionsTypes
;
49 use Galette\Repository\PaymentTypes
;
52 * Galette contributions controller
54 * @category Controllers
55 * @name ContributionsController
57 * @author Johan Cwiklinski <johan@x-tnd.be>
58 * @copyright 2020-2023 The Galette Team
59 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
60 * @link http://galette.tuxfamily.org
61 * @since Available since 0.9.4dev - 2020-05-02
64 class ContributionsController
extends CrudController
73 * Only a few things changes in add and edit pages,
74 * boths methods will use this common one.
76 * @param Request $request PSR Request
77 * @param Response $response PSR Response
78 * @param string $type Contribution type
79 * @param Contribution $contrib Contribution instance
83 public function addEditPage(
89 $post = $request->getParsedBody();
91 // check for ajax mode
94 ($request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest')
95 ||
isset($post['ajax'])
96 && $post['ajax'] == 'true'
101 // contribution types
102 $ct = new ContributionsTypes($this->zdb
);
103 $contributions_types = $ct->getList($type === Contribution
::TYPE_FEE
);
105 // template variable declaration
107 if ($type === Contribution
::TYPE_FEE
) {
108 $title = _T("Membership fee");
110 $title = _T("Donation");
113 if ($contrib->id
!= '') {
114 $title .= ' (' . _T("modification") . ')';
116 $title .= ' (' . _T("creation") . ')';
120 'page_title' => $title,
121 'required' => $contrib->getRequired(),
122 'contribution' => $contrib,
123 'adh_selected' => $contrib->member
,
127 // contribution types
128 $params['type_cotis_options'] = $contributions_types;
132 $members = $m->getDropdownMembers(
135 $contrib->member
> 0 ?
$contrib->member
: null
138 $params['members'] = [
139 'filters' => $m->getFilters(),
140 'count' => $m->getCount()
143 if (count($members)) {
144 $params['members']['list'] = $members;
147 $ext_membership = '';
148 if ($contrib->isFee() ||
$type === Contribution
::TYPE_FEE
) {
149 $ext_membership = $this->preferences
->pref_membership_ext
;
151 $params['pref_membership_ext'] = $ext_membership;
152 $params['autocomplete'] = true;
153 $params['mode'] = ($ajax ?
'ajax' : '');
158 'pages/contribution_form.html.twig',
167 * @param Request $request PSR Request
168 * @param Response $response PSR Response
169 * @param string|null $type Contribution type
173 public function add(Request
$request, Response
$response, string $type = null): Response
175 if ($this->session
->contribution
!== null) {
176 $contrib = $this->session
->contribution
;
177 $this->session
->contribution
= null;
179 $get = $request->getQueryParams();
181 $ct = new ContributionsTypes($this->zdb
);
182 $contributions_types = $ct->getList($type === Contribution
::TYPE_FEE
);
184 $cparams = ['type' => array_keys($contributions_types)[0]];
187 if (isset($get[Adherent
::PK
]) && $get[Adherent
::PK
] > 0) {
188 $cparams['adh'] = (int)$get[Adherent
::PK
];
192 if (isset($get[Transaction
::PK
]) && $get[Transaction
::PK
] > 0) {
193 $cparams['trans'] = $get[Transaction
::PK
];
196 $contrib = new Contribution(
202 if (isset($cparams['adh'])) {
203 $contrib->member
= $cparams['adh'];
206 if (isset($get['montant_cotis']) && $get['montant_cotis'] > 0) {
207 $contrib->amount
= $get['montant_cotis'];
211 return $this->addEditPage($request, $response, $type, $contrib);
217 * @param Request $request PSR Request
218 * @param Response $response PSR Response
219 * @param string|null $type Contribution type
223 public function doAdd(Request
$request, Response
$response, string $type = null): Response
225 return $this->store($request, $response, 'add', $type);
229 * Choose contribution type to mass add contribution
231 * @param Request $request PSR Request
232 * @param Response $response PSR Response
236 public function massAddChooseType(Request
$request, Response
$response): Response
238 $filters = $this->session
->filter_members
;
240 'id' => $filters->selected
,
241 'redirect_uri' => $this->routeparser
->urlFor('members')
247 'modals/mass_choose_contributions_type.html.twig',
249 'mode' => ($request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest') ?
'ajax' : '',
250 'page_title' => str_replace(
253 _T('Mass add contribution on %count members')
256 'form_url' => $this->routeparser
->urlFor('massAddContributions'),
257 'cancel_uri' => $this->routeparser
->urlFor('members')
264 * Massive change page
266 * @param Request $request PSR Request
267 * @param Response $response PSR Response
271 public function massAddContributions(Request
$request, Response
$response): Response
273 $post = $request->getParsedBody();
274 $filters = $this->session
->filter_members
;
275 $type = $post['type'];
277 $ct = new ContributionsTypes($this->zdb
);
278 $contributions_types = $ct->getList($type === Contribution
::TYPE_FEE
);
280 $contribution = new Contribution(
283 ['type' => array_keys($contributions_types)[0]]
287 'id' => $filters->selected
,
288 'redirect_uri' => $this->routeparser
->urlFor('members'),
295 'modals/mass_add_contributions.html.twig',
297 'mode' => ($request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest') ?
'ajax' : '',
298 'page_title' => str_replace(
301 _T('Mass add contribution on %count members')
303 'form_url' => $this->routeparser
->urlFor('doMassAddContributions'),
304 'cancel_uri' => $this->routeparser
->urlFor('members'),
306 'contribution' => $contribution,
308 'require_mass' => true,
309 'required' => $contribution->getRequired(),
310 'type_cotis_options' => $contributions_types
317 * Do massive contribution add
319 * @param Request $request PSR Request
320 * @param Response $response PSR Response
324 public function doMassAddContributions(Request
$request, Response
$response): Response
326 $post = $request->getParsedBody();
327 $members_ids = $post['id'];
330 $error_detected = [];
332 // flagging required fields for first step only
337 foreach ($members_ids as $member_id) {
338 $post[Adherent
::PK
] = (int)$member_id;
339 $contrib = new Contribution($this->zdb
, $this->login
);
342 $valid = $contrib->check($post, $contrib->getRequired(), $disabled);
343 if ($valid !== true) {
344 $error_detected = array_merge($error_detected, $valid);
347 //all goes well, we can proceed
348 if (count($error_detected) == 0) {
349 $store = $contrib->store();
350 if ($store === true) {
352 $files_res = $contrib->handleFiles($_FILES);
353 if (is_array($files_res)) {
354 $error_detected = array_merge($error_detected, $files_res);
362 if (count($error_detected) == 0) {
363 $redirect_url = $this->routeparser
->urlFor('members');
365 //something went wrong.
366 //store entity in session
367 $redirect_url = $this->routeparser
->urlFor('massAddContributions');
369 foreach ($error_detected as $error) {
370 $this->flash
->addMessage(
377 //redirect to calling action
380 ->withHeader('Location', $redirect_url);
389 * @param Request $request PSR Request
390 * @param Response $response PSR Response
391 * @param string $option One of 'page' or 'order'
392 * @param string|integer $value Value of the option
393 * @param string $type One of 'transactions' or 'contributions'
397 public function list(Request
$request, Response
$response, $option = null, $value = null, $type = null): Response
400 $get = $request->getQueryParams();
404 $raw_type = 'transactions';
406 case 'contributions':
407 $raw_type = 'contributions';
411 'Trying to load unknown contribution type ' . $type,
418 $this->routeparser
->urlFor('me')
422 $filter_name = 'filter_' . $raw_type;
424 ($request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest')
425 ||
isset($get['ajax'])
426 && $get['ajax'] == 'true'
429 $filter_name .= '_ajax';
432 if (isset($this->session
->$filter_name)) {
433 $filters = $this->session
->$filter_name;
435 $filter_class = '\\Galette\\Filters\\' . ucwords($raw_type . 'List');
436 $filters = new $filter_class();
440 if (isset($get[Adherent
::PK
]) && $get[Adherent
::PK
] > 0) {
441 $filters->filtre_cotis_adh
= (int)$get[Adherent
::PK
];
444 if ($type === 'contributions') {
445 if (isset($request->getQueryParams()['max_amount'])) {
446 $filters->filtre_transactions
= true;
447 $filters->max_amount
= (int)$request->getQueryParams()['max_amount'];
451 if ($option !== null) {
454 $filters->current_page
= (int)$value;
457 $filters->orderby
= $value;
460 $filters->filtre_cotis_adh
= ($value === 'all' ?
null : $value);
465 if (!$this->login
->isAdmin() && !$this->login
->isStaff() && $value != $this->login
->id
) {
466 if ($value === 'all' ||
empty($value)) {
467 $value = $this->login
->id
;
469 $member = new Adherent(
480 !$member->hasParent() ||
481 $member->parent
->id
!= $this->login
->id
483 $value = $this->login
->id
;
485 'Trying to display contributions for member #' . $value .
486 ' without appropriate ACLs',
491 $filters->filtre_cotis_children
= $value;
494 $class = '\\Galette\\Entity\\' . ucwords(trim($raw_type, 's'));
495 $contrib = new $class($this->zdb
, $this->login
);
497 if (!$contrib->canShow($this->login
)) {
499 'Trying to display contributions without appropriate ACLs',
506 $this->routeparser
->urlFor('me')
510 $class = '\\Galette\\Repository\\' . ucwords($raw_type);
511 $contrib = new $class($this->zdb
, $this->login
, $filters);
512 $contribs_list = $contrib->getList(true);
514 //store filters into session
515 if ($ajax === false) {
516 $this->session
->$filter_name = $filters;
519 //assign pagination variables to the template and add pagination links
520 $filters->setSmartyPagination($this->routeparser
, $this->view
);
523 'page_title' => $raw_type === 'contributions' ?
524 _T("Contributions management") : _T("Transactions management"),
525 'contribs' => $contrib,
526 'list' => $contribs_list,
527 'nb' => $contrib->getCount(),
528 'filters' => $filters,
529 'mode' => ($ajax === true ?
'ajax' : 'std')
532 if ($filters->filtre_cotis_adh
!= null) {
533 $member = new Adherent($this->zdb
);
534 $member->enableDep('children');
535 $member->load($filters->filtre_cotis_adh
);
536 $tpl_vars['member'] = $member;
539 if ($filters->filtre_cotis_children
!= false) {
540 $member = new Adherent(
542 $filters->filtre_cotis_children
,
550 $tpl_vars['pmember'] = $member;
553 // hide column action in ajax mode
554 if ($ajax === true) {
555 $tpl_vars['no_action'] = true;
561 'pages/' . $raw_type . '_list.html.twig',
568 * List page for logged-in member
570 * @param Request $request PSR Request
571 * @param Response $response PSR Response
572 * @param string $type One of 'transactions' or 'contributions'
576 public function myList(Request
$request, Response
$response, string $type = null): Response
579 $request->withQueryParams(
580 $request->getQueryParams() +
[
581 Adherent
::PK
=> $this->login
->id
594 * @param Request $request PSR Request
595 * @param Response $response PSR Response
596 * @param string|null $type One of 'transactions' or 'contributions'
600 public function filter(Request
$request, Response
$response, string $type = null): Response
603 $filter_name = 'filter_' . $type;
604 if ($request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest') {
606 $filter_name .= '_ajax';
609 $post = $request->getParsedBody();
610 $error_detected = [];
612 if ($this->session
->$filter_name !== null) {
613 $filters = $this->session
->$filter_name;
615 $filter_class = '\\Galette\\Filters\\' . ucwords($type) . 'List';
616 $filters = new $filter_class();
619 if (isset($post['clear_filter'])) {
620 $filters->reinit($ajax);
622 if (!isset($post['max_amount'])) {
623 $filters->max_amount
= null;
627 (isset($post['nbshow']) && is_numeric($post['nbshow']))
629 $filters->show
= $post['nbshow'];
632 if (isset($post['date_field'])) {
633 $filters->date_field
= $post['date_field'];
636 if (isset($post['end_date_filter']) ||
isset($post['start_date_filter'])) {
637 if (isset($post['start_date_filter'])) {
638 $filters->start_date_filter
= $post['start_date_filter'];
640 if (isset($post['end_date_filter'])) {
641 $filters->end_date_filter
= $post['end_date_filter'];
645 if (isset($post['payment_type_filter'])) {
646 $ptf = (int)$post['payment_type_filter'];
647 $ptypes = new PaymentTypes(
652 $ptlist = $ptypes->getList();
653 if (isset($ptlist[$ptf])) {
654 $filters->payment_type_filter
= $ptf;
655 } elseif ($ptf == -1) {
656 $filters->payment_type_filter
= null;
658 $error_detected[] = _T("- Unknown payment type!");
663 $this->session
->$filter_name = $filters;
665 if (count($error_detected) > 0) {
667 foreach ($error_detected as $error) {
668 $this->flash
->addMessage(
677 ->withHeader('Location', $this->routeparser
->urlFor('contributions', ['type' => $type]));
681 * Batch actions handler
683 * @param Request $request PSR Request
684 * @param Response $response PSR Response
685 * @param string $type One of 'transactions' or 'contributions'
689 public function handleBatch(Request
$request, Response
$response, string $type): Response
691 $filter_name = 'filter_' . $type;
692 $post = $request->getParsedBody();
694 if (isset($post['entries_sel'])) {
695 $filter_class = '\\Galette\\Filters\\' . ucwords($type . 'List');
696 $filters = $this->session
->$filter_name ??
new $filter_class();
697 $filters->selected
= $post['entries_sel'];
698 $this->session
->$filter_name = $filters;
700 if (isset($post['csv'])) {
703 ->withHeader('Location', $this->routeparser
->urlFor('csv-contributionslist', ['type' => $type]));
706 if (isset($post['delete'])) {
709 ->withHeader('Location', $this->routeparser
->urlFor('removeContributions', ['type' => $type]));
712 throw new \
RuntimeException('Does not know what to batch :(');
714 $this->flash
->addMessage(
716 _T("No contribution was selected, please check at least one.")
721 ->withHeader('Location', $this->routeparser
->urlFor('contributions', ['type' => $type]));
731 * @param Request $request PSR Request
732 * @param Response $response PSR Response
733 * @param int $id Contribution id
734 * @param string|null $type Contribution type
738 public function edit(Request
$request, Response
$response, int $id, string $type = null): Response
740 if ($this->session
->contribution
!== null) {
741 $contrib = $this->session
->contribution
;
742 $this->session
->contribution
= null;
744 $contrib = new Contribution($this->zdb
, $this->login
, $id);
745 if ($contrib->id
== '') {
746 //not possible to load contribution, exit
747 $this->flash
->addMessage(
752 _T("Unable to load contribution #%id!")
757 ->withHeader('Location', $this->routeparser
->urlFor(
759 ['type' => 'contributions']
764 return $this->addEditPage($request, $response, $type, $contrib);
770 * @param Request $request PSR Request
771 * @param Response $response PSR Response
772 * @param integer $id Contribution id
773 * @param string|null $type Contribution type
777 public function doEdit(Request
$request, Response
$response, int $id, string $type = null): Response
779 return $this->store($request, $response, 'edit', $type, $id);
783 * Store contribution (new or existing)
785 * @param Request $request PSR Request
786 * @param Response $response PSR Response
787 * @param string $action Action ('edit' or 'add')
788 * @param string $type Contribution type
789 * @param integer $id Contribution id
793 public function store(Request
$request, Response
$response, $action, string $type, $id = null): Response
795 $post = $request->getParsedBody();
801 $url_args['id'] = $id;
804 if ($action == 'edit' && isset($post['btnreload'])) {
805 $redirect_url = $this->routeparser
->urlFor($action . 'Contribution', $url_args);
806 $redirect_url .= '?' . Adherent
::PK
. '=' . $post[Adherent
::PK
] . '&' .
807 ContributionsTypes
::PK
. '=' . $post[ContributionsTypes
::PK
] . '&' .
808 'montant_cotis=' . $post['montant_cotis'];
811 ->withHeader('Location', $redirect_url);
814 $error_detected = [];
816 if ($this->session
->contribution
!== null) {
817 $contrib = $this->session
->contribution
;
818 $this->session
->contribution
= null;
821 $contrib = new Contribution($this->zdb
, $this->login
);
823 $contrib = new Contribution($this->zdb
, $this->login
, $id);
830 $valid = $contrib->check($post, $contrib->getRequired(), $disabled);
831 if ($valid !== true) {
832 $error_detected = array_merge($error_detected, $valid);
835 // send email to member
836 if (isset($post['mail_confirm']) && $post['mail_confirm'] == '1') {
837 $contrib->setSendmail(); //flag to send creation email
840 //all goes well, we can proceed
841 if (count($error_detected) == 0) {
842 $store = $contrib->store();
843 if ($store === true) {
844 $this->flash
->addMessage(
846 _T('Contribution has been successfully stored')
849 //something went wrong :'(
850 $error_detected[] = _T("An error occurred while storing the contribution.");
854 if (count($error_detected) === 0) {
855 $files_res = $contrib->handleFiles($_FILES);
856 if (is_array($files_res)) {
857 $error_detected = array_merge($error_detected, $files_res);
861 if (count($error_detected) == 0) {
862 $this->session
->contribution
= null;
863 if ($contrib->isTransactionPart() && $contrib->transaction
->getMissingAmount() > 0) {
865 $redirect_url = $this->routeparser
->urlFor(
868 'type' => $post['contrib_type'] ??
$type
870 ) . '?' . Transaction
::PK
. '=' . $contrib->transaction
->id
.
871 '&' . Adherent
::PK
. '=' . $contrib->member
;
873 //contributions list for member
874 $redirect_url = $this->routeparser
->urlFor(
877 'type' => 'contributions'
879 ) . '?' . Adherent
::PK
. '=' . $contrib->member
;
882 //something went wrong.
883 //store entity in session
884 $this->session
->contribution
= $contrib;
885 $redirect_url = $this->routeparser
->urlFor($action . 'Contribution', $url_args);
888 foreach ($error_detected as $error) {
889 $this->flash
->addMessage(
896 //redirect to calling action
899 ->withHeader('Location', $redirect_url);
906 * Get redirection URI
908 * @param array $args Route arguments
912 public function redirectUri(array $args)
914 return $this->routeparser
->urlFor('contributions', ['type' => $args['type']]);
920 * @param array $args Route arguments
924 public function formUri(array $args)
926 return $this->routeparser
->urlFor(
927 'doRemoveContribution',
933 * Get confirmation removal page title
935 * @param array $args Route arguments
939 public function confirmRemoveTitle(array $args)
943 switch ($args['type']) {
945 $raw_type = 'transactions';
947 case 'contributions':
948 $raw_type = 'contributions';
952 if (isset($args['ids'])) {
954 _T('Remove %1$s %2$s'),
956 ($raw_type === 'contributions') ?
_T('contributions') : _T('transactions')
960 _T('Remove %1$s #%2$s'),
961 ($raw_type === 'contributions') ?
_T('contribution') : _T('transaction'),
970 * @param array $args Route arguments
971 * @param array $post POST values
975 protected function doDelete(array $args, array $post)
978 switch ($args['type']) {
980 $raw_type = 'transactions';
982 case 'contributions':
983 $raw_type = 'contributions';
987 $class = '\\Galette\Repository\\' . ucwords($raw_type);
988 $contribs = new $class($this->zdb
, $this->login
);
989 $rm = $contribs->remove($args['ids'] ??
$args['id'], $this->history
);
997 * Get filter name in session
999 * @param array|null $args Route arguments
1003 public function getFilterName(array $args = null): string
1005 return 'filter_' . $args['type'];