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
;
40 use Galette\Filters\ContributionsList
;
43 use Galette\Controllers\CrudController
;
44 use Slim\Psr7\Request
;
45 use Slim\Psr7\Response
;
46 use Galette\Entity\Adherent
;
47 use Galette\Entity\Contribution
;
48 use Galette\Entity\Transaction
;
49 use Galette\Repository\Members
;
50 use Galette\Entity\ContributionsTypes
;
51 use Galette\Repository\PaymentTypes
;
54 * Galette contributions controller
56 * @category Controllers
57 * @name ContributionsController
59 * @author Johan Cwiklinski <johan@x-tnd.be>
60 * @copyright 2020-2023 The Galette Team
61 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
62 * @link http://galette.tuxfamily.org
63 * @since Available since 0.9.4dev - 2020-05-02
66 class ContributionsController
extends CrudController
75 * Only a few things changes in add and edit pages,
76 * boths methods will use this common one.
78 * @param Request $request PSR Request
79 * @param Response $response PSR Response
80 * @param string $type Contribution type
81 * @param Contribution $contrib Contribution instance
85 public function addEditPage(
91 $post = $request->getParsedBody();
93 // check for ajax mode
96 ($request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest')
97 ||
isset($post['ajax'])
98 && $post['ajax'] == 'true'
103 // contribution types
104 $ct = new ContributionsTypes($this->zdb
);
105 $contributions_types = $ct->getList($type === Contribution
::TYPE_FEE
);
107 // template variable declaration
109 if ($type === Contribution
::TYPE_FEE
) {
110 $title = _T("Membership fee");
112 $title = _T("Donation");
115 if ($contrib->id
!= '') {
116 $title .= ' (' . _T("modification") . ')';
118 $title .= ' (' . _T("creation") . ')';
122 'page_title' => $title,
123 'required' => $contrib->getRequired(),
124 'contribution' => $contrib,
125 'adh_selected' => $contrib->member
,
129 // contribution types
130 $params['type_cotis_options'] = $contributions_types;
134 $members = $m->getDropdownMembers(
137 $contrib->member
> 0 ?
$contrib->member
: null
140 $params['members'] = [
141 'filters' => $m->getFilters(),
142 'count' => $m->getCount()
145 if (count($members)) {
146 $params['members']['list'] = $members;
149 $ext_membership = '';
150 if ($contrib->isFee() ||
$type === Contribution
::TYPE_FEE
) {
151 $ext_membership = $this->preferences
->pref_membership_ext
;
153 $params['pref_membership_ext'] = $ext_membership;
154 $params['autocomplete'] = true;
155 $params['mode'] = ($ajax ?
'ajax' : '');
160 'pages/contribution_form.html.twig',
169 * @param Request $request PSR Request
170 * @param Response $response PSR Response
171 * @param string|null $type Contribution type
175 public function add(Request
$request, Response
$response, string $type = null): Response
177 if ($this->session
->contribution
!== null) {
178 $contrib = $this->session
->contribution
;
179 $this->session
->contribution
= null;
181 $get = $request->getQueryParams();
183 $ct = new ContributionsTypes($this->zdb
);
184 $contributions_types = $ct->getList($type === Contribution
::TYPE_FEE
);
186 $cparams = ['type' => array_keys($contributions_types)[0]];
189 if (isset($get[Adherent
::PK
]) && $get[Adherent
::PK
] > 0) {
190 $cparams['adh'] = (int)$get[Adherent
::PK
];
194 if (isset($get[Transaction
::PK
]) && $get[Transaction
::PK
] > 0) {
195 $cparams['trans'] = $get[Transaction
::PK
];
198 $contrib = new Contribution(
204 if (isset($cparams['adh'])) {
205 $contrib->member
= $cparams['adh'];
208 if (isset($get['montant_cotis']) && $get['montant_cotis'] > 0) {
209 $contrib->amount
= $get['montant_cotis'];
213 return $this->addEditPage($request, $response, $type, $contrib);
219 * @param Request $request PSR Request
220 * @param Response $response PSR Response
221 * @param string|null $type Contribution type
225 public function doAdd(Request
$request, Response
$response, string $type = null): Response
227 return $this->store($request, $response, 'add', $type);
231 * Choose contribution type to mass add contribution
233 * @param Request $request PSR Request
234 * @param Response $response PSR Response
238 public function massAddChooseType(Request
$request, Response
$response): Response
240 $filters = $this->session
->filter_members
;
242 'id' => $filters->selected
,
243 'redirect_uri' => $this->routeparser
->urlFor('members')
249 'modals/mass_choose_contributions_type.html.twig',
251 'mode' => ($request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest') ?
'ajax' : '',
252 'page_title' => str_replace(
255 _T('Mass add contribution on %count members')
258 'form_url' => $this->routeparser
->urlFor('massAddContributions'),
259 'cancel_uri' => $this->routeparser
->urlFor('members')
266 * Massive change page
268 * @param Request $request PSR Request
269 * @param Response $response PSR Response
273 public function massAddContributions(Request
$request, Response
$response): Response
275 $post = $request->getParsedBody();
276 $filters = $this->session
->filter_members
;
277 $type = $post['type'];
279 $ct = new ContributionsTypes($this->zdb
);
280 $contributions_types = $ct->getList($type === Contribution
::TYPE_FEE
);
282 $contribution = new Contribution(
285 ['type' => array_keys($contributions_types)[0]]
289 'id' => $filters->selected
,
290 'redirect_uri' => $this->routeparser
->urlFor('members'),
297 'modals/mass_add_contributions.html.twig',
299 'mode' => ($request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest') ?
'ajax' : '',
300 'page_title' => str_replace(
303 _T('Mass add contribution on %count members')
305 'form_url' => $this->routeparser
->urlFor('doMassAddContributions'),
306 'cancel_uri' => $this->routeparser
->urlFor('members'),
308 'contribution' => $contribution,
310 'require_mass' => true,
311 'required' => $contribution->getRequired(),
312 'type_cotis_options' => $contributions_types
319 * Do massive contribution add
321 * @param Request $request PSR Request
322 * @param Response $response PSR Response
326 public function doMassAddContributions(Request
$request, Response
$response): Response
328 $post = $request->getParsedBody();
329 $members_ids = $post['id'];
332 $error_detected = [];
334 // flagging required fields for first step only
339 foreach ($members_ids as $member_id) {
340 $post[Adherent
::PK
] = (int)$member_id;
341 $contrib = new Contribution($this->zdb
, $this->login
);
344 $valid = $contrib->check($post, $contrib->getRequired(), $disabled);
345 if ($valid !== true) {
346 $error_detected = array_merge($error_detected, $valid);
349 //all goes well, we can proceed
350 if (count($error_detected) == 0) {
351 $store = $contrib->store();
352 if ($store === true) {
354 $files_res = $contrib->handleFiles($_FILES);
355 if (is_array($files_res)) {
356 $error_detected = array_merge($error_detected, $files_res);
364 if (count($error_detected) == 0) {
365 $redirect_url = $this->routeparser
->urlFor('members');
367 //something went wrong.
368 //store entity in session
369 $redirect_url = $this->routeparser
->urlFor('massAddContributions');
371 foreach ($error_detected as $error) {
372 $this->flash
->addMessage(
379 //redirect to calling action
382 ->withHeader('Location', $redirect_url);
391 * @param Request $request PSR Request
392 * @param Response $response PSR Response
393 * @param string $option One of 'page' or 'order'
394 * @param string|integer $value Value of the option
395 * @param string $type One of 'transactions' or 'contributions'
399 public function list(Request
$request, Response
$response, $option = null, $value = null, $type = null): Response
402 $get = $request->getQueryParams();
406 $raw_type = 'transactions';
408 case 'contributions':
409 $raw_type = 'contributions';
413 'Trying to load unknown contribution type ' . $type,
420 $this->routeparser
->urlFor('me')
424 $filter_name = 'filter_' . $raw_type;
426 ($request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest')
427 ||
isset($get['ajax'])
428 && $get['ajax'] == 'true'
431 $filter_name .= '_ajax';
434 if (isset($this->session
->$filter_name)) {
435 $filters = $this->session
->$filter_name;
437 $filter_class = '\\Galette\\Filters\\' . ucwords($raw_type . 'List');
438 $filters = new $filter_class();
442 if (isset($get[Adherent
::PK
]) && $get[Adherent
::PK
] > 0) {
443 $filters->filtre_cotis_adh
= (int)$get[Adherent
::PK
];
446 if ($type === 'contributions') {
447 if (isset($request->getQueryParams()['max_amount'])) {
448 $filters->filtre_transactions
= true;
449 $filters->max_amount
= (int)$request->getQueryParams()['max_amount'];
453 if ($option !== null) {
456 $filters->current_page
= (int)$value;
459 $filters->orderby
= $value;
462 $filters->filtre_cotis_adh
= ($value === 'all' ?
null : $value);
467 if (!$this->login
->isAdmin() && !$this->login
->isStaff() && $value != $this->login
->id
) {
468 if ($value === 'all' ||
empty($value)) {
469 $value = $this->login
->id
;
471 $member = new Adherent(
482 !$member->hasParent() ||
483 $member->hasParent() && $member->parent
->id
!= $this->login
->id
485 $value = $this->login
->id
;
487 'Trying to display contributions for member #' . $value .
488 ' without appropriate ACLs',
493 $filters->filtre_cotis_children
= $value;
496 $class = '\\Galette\\Entity\\' . ucwords(trim($raw_type, 's'));
497 $contrib = new $class($this->zdb
, $this->login
);
499 if (!$contrib->canShow($this->login
)) {
501 'Trying to display contributions without appropriate ACLs',
508 $this->routeparser
->urlFor('me')
512 $class = '\\Galette\\Repository\\' . ucwords($raw_type);
513 $contrib = new $class($this->zdb
, $this->login
, $filters);
514 $contribs_list = $contrib->getList(true);
516 //store filters into session
517 if ($ajax === false) {
518 $this->session
->$filter_name = $filters;
521 //assign pagination variables to the template and add pagination links
522 $filters->setSmartyPagination($this->routeparser
, $this->view
);
525 'page_title' => $raw_type === 'contributions' ?
526 _T("Contributions management") : _T("Transactions management"),
527 'contribs' => $contrib,
528 'list' => $contribs_list,
529 'nb' => $contrib->getCount(),
530 'filters' => $filters,
531 'mode' => ($ajax === true ?
'ajax' : 'std')
534 if ($filters->filtre_cotis_adh
!= null) {
535 $member = new Adherent($this->zdb
);
536 $member->enableDep('children');
537 $member->load($filters->filtre_cotis_adh
);
538 $tpl_vars['member'] = $member;
541 if ($filters->filtre_cotis_children
!= false) {
542 $member = new Adherent(
544 $filters->filtre_cotis_children
,
552 $tpl_vars['pmember'] = $member;
555 // hide column action in ajax mode
556 if ($ajax === true) {
557 $tpl_vars['no_action'] = true;
563 'pages/' . $raw_type . '_list.html.twig',
570 * List page for logged-in member
572 * @param Request $request PSR Request
573 * @param Response $response PSR Response
574 * @param string $type One of 'transactions' or 'contributions'
578 public function myList(Request
$request, Response
$response, string $type = null): Response
581 $request->withQueryParams(
582 $request->getQueryParams() +
[
583 Adherent
::PK
=> $this->login
->id
596 * @param Request $request PSR Request
597 * @param Response $response PSR Response
598 * @param string|null $type One of 'transactions' or 'contributions'
602 public function filter(Request
$request, Response
$response, string $type = null): Response
605 $filter_name = 'filter_' . $type;
606 if ($request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest') {
608 $filter_name .= '_ajax';
611 $post = $request->getParsedBody();
612 $error_detected = [];
614 if ($this->session
->$filter_name !== null) {
615 $filters = $this->session
->$filter_name;
617 $filter_class = '\\Galette\\Filters\\' . ucwords($type) . 'List';
618 $filters = new $filter_class();
621 if (isset($post['clear_filter'])) {
622 $filters->reinit($ajax);
624 if (!isset($post['max_amount'])) {
625 $filters->max_amount
= null;
629 (isset($post['nbshow']) && is_numeric($post['nbshow']))
631 $filters->show
= $post['nbshow'];
634 if (isset($post['date_field'])) {
635 $filters->date_field
= $post['date_field'];
638 if (isset($post['end_date_filter']) ||
isset($post['start_date_filter'])) {
640 if (isset($post['start_date_filter'])) {
641 $filters->start_date_filter
= $post['start_date_filter'];
643 if (isset($post['end_date_filter'])) {
644 $filters->end_date_filter
= $post['end_date_filter'];
646 } catch (Throwable
$e) {
647 $error_detected[] = $e->getMessage();
651 if (isset($post['payment_type_filter'])) {
652 $ptf = (int)$post['payment_type_filter'];
653 $ptypes = new PaymentTypes(
658 $ptlist = $ptypes->getList();
659 if (isset($ptlist[$ptf])) {
660 $filters->payment_type_filter
= $ptf;
661 } elseif ($ptf == -1) {
662 $filters->payment_type_filter
= null;
664 $error_detected[] = _T("- Unknown payment type!");
669 $this->session
->$filter_name = $filters;
671 if (count($error_detected) > 0) {
673 foreach ($error_detected as $error) {
674 $this->flash
->addMessage(
683 ->withHeader('Location', $this->routeparser
->urlFor('contributions', ['type' => $type]));
687 * Batch actions handler
689 * @param Request $request PSR Request
690 * @param Response $response PSR Response
691 * @param string $type One of 'transactions' or 'contributions'
695 public function handleBatch(Request
$request, Response
$response, string $type): Response
697 $filter_name = 'filter_' . $type;
698 $post = $request->getParsedBody();
700 if (isset($post['entries_sel'])) {
701 $filters = $this->session
->$filter_name ??
new ContributionsList();
702 $filters->selected
= $post['entries_sel'];
703 $this->session
->$filter_name = $filters;
705 if (isset($post['csv'])) {
708 ->withHeader('Location', $this->routeparser
->urlFor('csv-contributionslist', ['type' => $type]));
711 if (isset($post['delete'])) {
714 ->withHeader('Location', $this->routeparser
->urlFor('removeContributions', ['type' => $type]));
717 throw new \
RuntimeException('Does not know what to batch :(');
719 $this->flash
->addMessage(
721 _T("No contribution was selected, please check at least one.")
726 ->withHeader('Location', $this->routeparser
->urlFor('contributions', ['type' => $type]));
736 * @param Request $request PSR Request
737 * @param Response $response PSR Response
738 * @param int $id Contribution id
739 * @param string|null $type Contribution type
743 public function edit(Request
$request, Response
$response, int $id, string $type = null): Response
745 if ($this->session
->contribution
!== null) {
746 $contrib = $this->session
->contribution
;
747 $this->session
->contribution
= null;
749 $contrib = new Contribution($this->zdb
, $this->login
, $id);
750 if ($contrib->id
== '') {
751 //not possible to load contribution, exit
752 $this->flash
->addMessage(
757 _T("Unable to load contribution #%id!")
762 ->withHeader('Location', $this->routeparser
->urlFor(
764 ['type' => 'contributions']
769 return $this->addEditPage($request, $response, $type, $contrib);
775 * @param Request $request PSR Request
776 * @param Response $response PSR Response
777 * @param integer $id Contribution id
778 * @param string|null $type Contribution type
782 public function doEdit(Request
$request, Response
$response, int $id, string $type = null): Response
784 return $this->store($request, $response, 'edit', $type, $id);
788 * Store contribution (new or existing)
790 * @param Request $request PSR Request
791 * @param Response $response PSR Response
792 * @param string $action Action ('edit' or 'add')
793 * @param string $type Contribution type
794 * @param integer $id Contribution id
798 public function store(Request
$request, Response
$response, $action, string $type, $id = null): Response
800 $post = $request->getParsedBody();
809 if ($action == 'edit' && isset($post['btnreload'])) {
810 $redirect_url = $this->routeparser
->urlFor($action . 'Contribution', $args);
811 $redirect_url .= '?' . Adherent
::PK
. '=' . $post[Adherent
::PK
] . '&' .
812 ContributionsTypes
::PK
. '=' . $post[ContributionsTypes
::PK
] . '&' .
813 'montant_cotis=' . $post['montant_cotis'];
816 ->withHeader('Location', $redirect_url);
819 $error_detected = [];
821 if ($this->session
->contribution
!== null) {
822 $contrib = $this->session
->contribution
;
823 $this->session
->contribution
= null;
826 $contrib = new Contribution($this->zdb
, $this->login
);
828 $contrib = new Contribution($this->zdb
, $this->login
, $id);
835 $valid = $contrib->check($post, $contrib->getRequired(), $disabled);
836 if ($valid !== true) {
837 $error_detected = array_merge($error_detected, $valid);
840 // send email to member
841 if (isset($post['mail_confirm']) && $post['mail_confirm'] == '1') {
842 $contrib->setSendmail(); //flag to send creation email
845 //all goes well, we can proceed
846 if (count($error_detected) == 0) {
847 $store = $contrib->store();
848 if ($store === true) {
849 $this->flash
->addMessage(
851 _T('Contribution has been successfully stored')
854 //something went wrong :'(
855 $error_detected[] = _T("An error occurred while storing the contribution.");
859 if (count($error_detected) === 0) {
860 $files_res = $contrib->handleFiles($_FILES);
861 if (is_array($files_res)) {
862 $error_detected = array_merge($error_detected, $files_res);
866 if (count($error_detected) == 0) {
867 $this->session
->contribution
= null;
868 if ($contrib->isTransactionPart() && $contrib->transaction
->getMissingAmount() > 0) {
870 $redirect_url = $this->routeparser
->urlFor(
873 'type' => $post['contrib_type'] ??
$type
875 ) . '?' . Transaction
::PK
. '=' . $contrib->transaction
->id
.
876 '&' . Adherent
::PK
. '=' . $contrib->member
;
878 //contributions list for member
879 $redirect_url = $this->routeparser
->urlFor(
882 'type' => 'contributions'
884 ) . '?' . Adherent
::PK
. '=' . $contrib->member
;
887 //something went wrong.
888 //store entity in session
889 $this->session
->contribution
= $contrib;
890 $redirect_url = $this->routeparser
->urlFor($action . 'Contribution', $args);
893 foreach ($error_detected as $error) {
894 $this->flash
->addMessage(
901 //redirect to calling action
904 ->withHeader('Location', $redirect_url);
911 * Get redirection URI
913 * @param array $args Route arguments
917 public function redirectUri(array $args)
919 return $this->routeparser
->urlFor('contributions', ['type' => $args['type']]);
925 * @param array $args Route arguments
929 public function formUri(array $args)
931 return $this->routeparser
->urlFor(
932 'doRemoveContribution',
938 * Get confirmation removal page title
940 * @param array $args Route arguments
944 public function confirmRemoveTitle(array $args)
948 switch ($args['type']) {
950 $raw_type = 'transactions';
952 case 'contributions':
953 $raw_type = 'contributions';
957 if (isset($args['ids'])) {
959 _T('Remove %1$s %2$s'),
961 ($raw_type === 'contributions') ?
_T('contributions') : _T('transactions')
965 _T('Remove %1$s #%2$s'),
966 ($raw_type === 'contributions') ?
_T('contribution') : _T('transaction'),
975 * @param array $args Route arguments
976 * @param array $post POST values
980 protected function doDelete(array $args, array $post)
983 switch ($args['type']) {
985 $raw_type = 'transactions';
987 case 'contributions':
988 $raw_type = 'contributions';
992 $class = '\\Galette\Repository\\' . ucwords($raw_type);
993 $contribs = new $class($this->zdb
, $this->login
);
994 $rm = $contribs->remove($args['ids'] ??
$args['id'], $this->history
);
1002 * Get filter name in session
1004 * @param array|null $args Route arguments
1008 public function getFilterName(array $args = null): string
1010 return 'filter_' . $args['type'];