3 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
6 * Galette contributions controller
10 * Copyright © 2020-2022 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-2022 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\Filters\ContributionsList
;
42 use Galette\Controllers\CrudController
;
43 use Slim\Http\Request
;
44 use Slim\Http\Response
;
45 use Galette\Entity\Adherent
;
46 use Galette\Entity\Contribution
;
47 use Galette\Entity\Transaction
;
48 use Galette\Repository\Members
;
49 use Galette\Entity\ContributionsTypes
;
50 use Galette\Repository\PaymentTypes
;
53 * Galette contributions controller
55 * @category Controllers
56 * @name ContributionsController
58 * @author Johan Cwiklinski <johan@x-tnd.be>
59 * @copyright 2020-2022 The Galette Team
60 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
61 * @link http://galette.tuxfamily.org
62 * @since Available since 0.9.4dev - 2020-05-02
65 class ContributionsController
extends CrudController
72 * Only a few things changes in add and edit pages,
73 * boths methods will use this common one.
75 * @param Request $request PSR Request
76 * @param Response $response PSR Response
77 * @param string $type Contribution type
78 * @param Contribution $contrib Contribution instance
82 public function addEditPage(
88 $post = $request->getParsedBody();
90 // check for ajax mode
94 ||
isset($post['ajax'])
95 && $post['ajax'] == 'true'
100 // contribution types
101 $ct = new ContributionsTypes($this->zdb
);
102 $contributions_types = $ct->getList($type === Contribution
::TYPE_FEE
);
104 // template variable declaration
106 if ($type === Contribution
::TYPE_FEE
) {
107 $title = _T("Membership fee");
109 $title = _T("Donation");
112 if ($contrib->id
!= '') {
113 $title .= ' (' . _T("modification") . ')';
115 $title .= ' (' . _T("creation") . ')';
119 'page_title' => $title,
120 'required' => $contrib->getRequired(),
121 'contribution' => $contrib,
122 'adh_selected' => $contrib->member
,
126 // contribution types
127 $params['type_cotis_options'] = $contributions_types;
131 $members = $m->getDropdownMembers(
134 $contrib->member
> 0 ?
$contrib->member
: null
137 $params['members'] = [
138 'filters' => $m->getFilters(),
139 'count' => $m->getCount()
142 if (count($members)) {
143 $params['members']['list'] = $members;
146 $ext_membership = '';
147 if ($contrib->isFee() ||
!isset($contrib) && $type === Contribution
::TYPE_FEE
) {
148 $ext_membership = $this->preferences
->pref_membership_ext
;
150 $params['pref_membership_ext'] = $ext_membership;
151 $params['autocomplete'] = true;
152 $params['mode'] = ($ajax ?
'ajax' : '');
157 'pages/contribution_form.html.twig',
166 * @param Request $request PSR Request
167 * @param Response $response PSR Response
168 * @param string|null $type Contribution type
172 public function add(Request
$request, Response
$response, string $type = null): Response
174 if ($this->session
->contribution
!== null) {
175 $contrib = $this->session
->contribution
;
176 $this->session
->contribution
= null;
178 $get = $request->getQueryParams();
180 $ct = new ContributionsTypes($this->zdb
);
181 $contributions_types = $ct->getList($type === Contribution
::TYPE_FEE
);
183 $cparams = ['type' => array_keys($contributions_types)[0]];
186 if (isset($get[Adherent
::PK
]) && $get[Adherent
::PK
] > 0) {
187 $cparams['adh'] = (int)$get[Adherent
::PK
];
191 if (isset($get[Transaction
::PK
]) && $get[Transaction
::PK
] > 0) {
192 $cparams['trans'] = $get[Transaction
::PK
];
195 $contrib = new Contribution(
198 (count($cparams) > 0 ?
$cparams : null)
201 if (isset($cparams['adh'])) {
202 $contrib->member
= $cparams['adh'];
205 if (isset($get['montant_cotis']) && $get['montant_cotis'] > 0) {
206 $contrib->amount
= $get['montant_cotis'];
210 return $this->addEditPage($request, $response, $type, $contrib);
216 * @param Request $request PSR Request
217 * @param Response $response PSR Response
218 * @param string|null $type Contribution type
222 public function doAdd(Request
$request, Response
$response, string $type = null): Response
224 return $this->store($request, $response, 'add', $type);
228 * Choose contribution type to mass add contribution
230 * @param Request $request PSR Request
231 * @param Response $response PSR Response
235 public function massAddChooseType(Request
$request, Response
$response): Response
237 $filters = $this->session
->filter_members
;
239 'id' => $filters->selected
,
240 'redirect_uri' => $this->router
->pathFor('members')
246 'modals/mass_choose_contributions_type.html.twig',
248 'mode' => $request->isXhr() ?
'ajax' : '',
249 'page_title' => str_replace(
252 _T('Mass add contribution on %count members')
255 'form_url' => $this->router
->pathFor('massAddContributions'),
256 'cancel_uri' => $this->router
->pathFor('members')
263 * Massive change page
265 * @param Request $request PSR Request
266 * @param Response $response PSR Response
270 public function massAddContributions(Request
$request, Response
$response): Response
272 $post = $request->getParsedBody();
273 $filters = $this->session
->filter_members
;
274 $contribution = new Contribution($this->zdb
, $this->login
);
276 $type = $post['type'];
278 'id' => $filters->selected
,
279 'redirect_uri' => $this->router
->pathFor('members'),
283 // contribution types
284 $ct = new ContributionsTypes($this->zdb
);
285 $contributions_types = $ct->getList($type === Contribution
::TYPE_FEE
);
290 'modals/mass_add_contributions.html.twig',
292 'mode' => $request->isXhr() ?
'ajax' : '',
293 'page_title' => str_replace(
296 _T('Mass add contribution on %count members')
298 'form_url' => $this->router
->pathFor('doMassAddContributions'),
299 'cancel_uri' => $this->router
->pathFor('members'),
301 'contribution' => $contribution,
303 'require_mass' => true,
304 'required' => $contribution->getRequired(),
305 'type_cotis_options' => $contributions_types
312 * Do massive contribution add
314 * @param Request $request PSR Request
315 * @param Response $response PSR Response
319 public function doMassAddContributions(Request
$request, Response
$response): Response
321 $post = $request->getParsedBody();
322 $members_ids = $post['id'];
325 $error_detected = [];
327 // flagging required fields for first step only
332 foreach ($members_ids as $member_id) {
333 $post[Adherent
::PK
] = (int)$member_id;
334 $contrib = new Contribution($this->zdb
, $this->login
);
337 $valid = $contrib->check($post, $contrib->getRequired(), $disabled);
338 if ($valid !== true) {
339 $error_detected = array_merge($error_detected, $valid);
342 //all goes well, we can proceed
343 if (count($error_detected) == 0) {
344 $store = $contrib->store();
345 if ($store === true) {
347 $files_res = $contrib->handleFiles($_FILES);
348 if (is_array($files_res)) {
349 $error_detected = array_merge($error_detected, $files_res);
357 if (count($error_detected) == 0) {
358 $redirect_url = $this->router
->pathFor('members');
360 //something went wrong.
361 //store entity in session
362 $redirect_url = $this->router
->pathFor('massAddContributions');
364 foreach ($error_detected as $error) {
365 $this->flash
->addMessage(
372 //redirect to calling action
375 ->withHeader('Location', $redirect_url);
384 * @param Request $request PSR Request
385 * @param Response $response PSR Response
386 * @param string $option One of 'page' or 'order'
387 * @param string|integer $value Value of the option
388 * @param string $type One of 'transactions' or 'contributions'
392 public function list(Request
$request, Response
$response, $option = null, $value = null, $type = null): Response
395 $get = $request->getQueryParams();
399 ||
isset($get['ajax'])
400 && $get['ajax'] == 'true'
407 $raw_type = 'transactions';
409 case 'contributions':
410 $raw_type = 'contributions';
414 'Trying to load unknown contribution type ' . $type,
421 $this->router
->pathFor('me')
425 $filter_name = 'filter_' . $raw_type;
427 if (isset($this->session
->$filter_name) && $ajax === false) {
428 $filters = $this->session
->$filter_name;
430 $filter_class = '\\Galette\\Filters\\' . ucwords($raw_type . 'List');
431 $filters = new $filter_class();
435 if (isset($get[Adherent
::PK
]) && $get[Adherent
::PK
] > 0) {
436 $filters->filtre_cotis_adh
= (int)$get[Adherent
::PK
];
439 $filters->filtre_transactions
= false;
440 if (isset($request->getQueryParams()['max_amount'])) {
441 $filters->filtre_transactions
= true;
442 $filters->max_amount
= (int)$request->getQueryParams()['max_amount'];
445 if ($option !== null) {
448 $filters->current_page
= (int)$value;
451 $filters->orderby
= $value;
454 $filters->filtre_cotis_adh
= ($value === 'all' ?
null : $value);
459 if (!$this->login
->isAdmin() && !$this->login
->isStaff() && $value != $this->login
->id
) {
460 if ($value === 'all' ||
empty($value)) {
461 $value = $this->login
->id
;
463 $member = new Adherent(
474 !$member->hasParent() ||
475 $member->hasParent() && $member->parent
->id
!= $this->login
->id
477 $value = $this->login
->id
;
479 'Trying to display contributions for member #' . $value .
480 ' without appropriate ACLs',
485 $filters->filtre_cotis_children
= $value;
488 $class = '\\Galette\\Entity\\' . ucwords(trim($raw_type, 's'));
489 $contrib = new $class($this->zdb
, $this->login
);
491 if (!$contrib->canShow($this->login
)) {
493 'Trying to display contributions without appropriate ACLs',
500 $this->router
->pathFor('me')
504 $class = '\\Galette\\Repository\\' . ucwords($raw_type);
505 $contrib = new $class($this->zdb
, $this->login
, $filters);
506 $contribs_list = $contrib->getList(true);
508 //store filters into session
509 if ($ajax === false) {
510 $this->session
->$filter_name = $filters;
513 //assign pagination variables to the template and add pagination links
514 $filters->setSmartyPagination($this->router
, $this->view
);
517 'page_title' => $raw_type === 'contributions' ?
518 _T("Contributions management") : _T("Transactions management"),
519 'contribs' => $contrib,
520 'list' => $contribs_list,
521 'nb' => $contrib->getCount(),
522 'filters' => $filters,
523 'mode' => ($ajax === true ?
'ajax' : 'std')
526 if ($filters->filtre_cotis_adh
!= null) {
527 $member = new Adherent($this->zdb
);
528 $member->load($filters->filtre_cotis_adh
);
529 $tpl_vars['member'] = $member;
532 if ($filters->filtre_cotis_children
!= false) {
533 $member = new Adherent(
535 $filters->filtre_cotis_children
,
543 $tpl_vars['pmember'] = $member;
546 // hide column action in ajax mode
547 if ($ajax === true) {
548 $tpl_vars['no_action'] = true;
554 'pages/' . $raw_type . '_list.html.twig',
561 * List page for logged-in member
563 * @param Request $request PSR Request
564 * @param Response $response PSR Response
565 * @param string $type One of 'transactions' or 'contributions'
569 public function myList(Request
$request, Response
$response, string $type = null): Response
572 $request->withQueryParams(
573 $request->getQueryParams() +
[
574 Adherent
::PK
=> $this->login
->id
587 * @param Request $request PSR Request
588 * @param Response $response PSR Response
589 * @param string|null $type One of 'transactions' or 'contributions'
593 public function filter(Request
$request, Response
$response, string $type = null): Response
595 $filter_name = 'filter_' . $type;
596 $post = $request->getParsedBody();
597 $error_detected = [];
599 if ($this->session
->$filter_name !== null) {
600 $filters = $this->session
->$filter_name;
602 $filter_class = '\\Galette\\Filters\\' . ucwords($type) . 'List';
603 $filters = new $filter_class();
606 if (isset($post['clear_filter'])) {
609 if (isset($post['max_amount'])) {
610 $filters->max_amount
= null;
614 (isset($post['nbshow']) && is_numeric($post['nbshow']))
616 $filters->show
= $post['nbshow'];
619 if (isset($post['date_field'])) {
620 $filters->date_field
= $post['date_field'];
623 if (isset($post['end_date_filter']) ||
isset($post['start_date_filter'])) {
625 if (isset($post['start_date_filter'])) {
626 $filters->start_date_filter
= $post['start_date_filter'];
628 if (isset($post['end_date_filter'])) {
629 $filters->end_date_filter
= $post['end_date_filter'];
631 } catch (Throwable
$e) {
632 $error_detected[] = $e->getMessage();
636 if (isset($post['payment_type_filter'])) {
637 $ptf = (int)$post['payment_type_filter'];
638 $ptypes = new PaymentTypes(
643 $ptlist = $ptypes->getList();
644 if (isset($ptlist[$ptf])) {
645 $filters->payment_type_filter
= $ptf;
646 } elseif ($ptf == -1) {
647 $filters->payment_type_filter
= null;
649 $error_detected[] = _T("- Unknown payment type!");
654 $this->session
->$filter_name = $filters;
656 if (count($error_detected) > 0) {
658 foreach ($error_detected as $error) {
659 $this->flash
->addMessage(
668 ->withHeader('Location', $this->router
->pathFor('contributions', ['type' => $type]));
672 * Batch actions handler
674 * @param Request $request PSR Request
675 * @param Response $response PSR Response
676 * @param string $type One of 'transactions' or 'contributions'
680 public function handleBatch(Request
$request, Response
$response, string $type): Response
682 $filter_name = 'filter_' . $type;
683 $post = $request->getParsedBody();
685 if (isset($post['entries_sel'])) {
686 $filters = $this->session
->$filter_name ??
new ContributionsList();
687 $filters->selected
= $post['entries_sel'];
688 $this->session
->$filter_name = $filters;
690 if (isset($post['csv'])) {
693 ->withHeader('Location', $this->router
->pathFor('csv-contributionslist', ['type' => $type]));
696 if (isset($post['delete'])) {
699 ->withHeader('Location', $this->router
->pathFor('removeContributions'));
702 throw new \
RuntimeException('Does not know what to batch :(');
704 $this->flash
->addMessage(
706 _T("No contribution was selected, please check at least one.")
711 ->withHeader('Location', $this->router
->pathFor('contributions', ['type' => $type]));
721 * @param Request $request PSR Request
722 * @param Response $response PSR Response
723 * @param int $id Contribution id
724 * @param string|null $type Contribution type
728 public function edit(Request
$request, Response
$response, int $id, string $type = null): Response
730 if ($this->session
->contribution
!== null) {
731 $contrib = $this->session
->contribution
;
732 $this->session
->contribution
= null;
734 $contrib = new Contribution($this->zdb
, $this->login
, $id);
735 if ($contrib->id
== '') {
736 //not possible to load contribution, exit
737 $this->flash
->addMessage(
742 _T("Unable to load contribution #%id!")
747 ->withHeader('Location', $this->router
->pathFor(
749 ['type' => 'contributions']
754 return $this->addEditPage($request, $response, $type, $contrib);
760 * @param Request $request PSR Request
761 * @param Response $response PSR Response
762 * @param integer $id Contribution id
763 * @param string|null $type Contribution type
767 public function doEdit(Request
$request, Response
$response, int $id, string $type = null): Response
769 return $this->store($request, $response, 'edit', $type, $id);
773 * Store contribution (new or existing)
775 * @param Request $request PSR Request
776 * @param Response $response PSR Response
777 * @param string $action Action ('edit' or 'add')
778 * @param string $type Contribution type
779 * @param integer $id Contribution id
783 public function store(Request
$request, Response
$response, $action, string $type, $id = null): Response
785 $post = $request->getParsedBody();
794 if ($action == 'edit' && isset($post['btnreload'])) {
795 $redirect_url = $this->router
->pathFor($action . 'Contribution', $args);
796 $redirect_url .= '?' . Adherent
::PK
. '=' . $post[Adherent
::PK
] . '&' .
797 ContributionsTypes
::PK
. '=' . $post[ContributionsTypes
::PK
] . '&' .
798 'montant_cotis=' . $post['montant_cotis'];
801 ->withHeader('Location', $redirect_url);
804 $error_detected = [];
806 if ($this->session
->contribution
!== null) {
807 $contrib = $this->session
->contribution
;
808 $this->session
->contribution
= null;
811 $contrib = new Contribution($this->zdb
, $this->login
);
813 $contrib = new Contribution($this->zdb
, $this->login
, $id);
820 $valid = $contrib->check($post, $contrib->getRequired(), $disabled);
821 if ($valid !== true) {
822 $error_detected = array_merge($error_detected, $valid);
825 // send email to member
826 if (isset($post['mail_confirm']) && $post['mail_confirm'] == '1') {
827 $contrib->setSendmail(); //flag to send creation email
830 //all goes well, we can proceed
831 if (count($error_detected) == 0) {
832 $store = $contrib->store();
833 if ($store === true) {
834 $this->flash
->addMessage(
836 _T('Contribution has been successfully stored')
839 //something went wrong :'(
840 $error_detected[] = _T("An error occurred while storing the contribution.");
844 if (count($error_detected) === 0) {
845 $files_res = $contrib->handleFiles($_FILES);
846 if (is_array($files_res)) {
847 $error_detected = array_merge($error_detected, $files_res);
851 if (count($error_detected) == 0) {
852 $this->session
->contribution
= null;
853 if ($contrib->isTransactionPart() && $contrib->transaction
->getMissingAmount() > 0) {
855 $redirect_url = $this->router
->pathFor(
858 'type' => $post['contrib_type']
860 ) . '?' . Transaction
::PK
. '=' . $contrib->transaction
->id
.
861 '&' . Adherent
::PK
. '=' . $contrib->member
;
863 //contributions list for member
864 $redirect_url = $this->router
->pathFor(
867 'type' => 'contributions'
869 ) . '?' . Adherent
::PK
. '=' . $contrib->member
;
872 //something went wrong.
873 //store entity in session
874 $this->session
->contribution
= $contrib;
875 $redirect_url = $this->router
->pathFor($action . 'Contribution', $args);
878 foreach ($error_detected as $error) {
879 $this->flash
->addMessage(
886 //redirect to calling action
889 ->withHeader('Location', $redirect_url);
896 * Get redirection URI
898 * @param array $args Route arguments
902 public function redirectUri(array $args)
904 return $this->router
->pathFor('contributions', ['type' => $args['type']]);
910 * @param array $args Route arguments
914 public function formUri(array $args)
916 return $this->router
->pathFor(
917 'doRemoveContribution',
923 * Get confirmation removal page title
925 * @param array $args Route arguments
929 public function confirmRemoveTitle(array $args)
933 switch ($args['type']) {
935 $raw_type = 'transactions';
937 case 'contributions':
938 $raw_type = 'contributions';
942 if (isset($args['ids'])) {
944 _T('Remove %1$s %2$s'),
946 ($raw_type === 'contributions') ?
_T('contributions') : _T('transactions')
950 _T('Remove %1$s #%2$s'),
951 ($raw_type === 'contributions') ?
_T('contribution') : _T('transaction'),
960 * @param array $args Route arguments
961 * @param array $post POST values
965 protected function doDelete(array $args, array $post)
968 switch ($args['type']) {
970 $raw_type = 'transactions';
972 case 'contributions':
973 $raw_type = 'contributions';
977 $class = '\\Galette\Repository\\' . ucwords($raw_type);
978 $contribs = new $class($this->zdb
, $this->login
);
979 $rm = $contribs->remove($args['ids'] ??
$args['id'], $this->history
);