]> git.agnieray.net Git - galette.git/commitdiff
Massive add contributions; closes #1381
authorJohan Cwiklinski <johan@x-tnd.be>
Thu, 30 Sep 2021 06:00:22 +0000 (08:00 +0200)
committerJohan Cwiklinski <johan@x-tnd.be>
Thu, 30 Sep 2021 09:06:00 +0000 (11:06 +0200)
galette/includes/routes/contributions.routes.php
galette/lib/Galette/Controllers/Crud/ContributionsController.php
galette/lib/Galette/Controllers/Crud/MembersController.php
galette/lib/Galette/Entity/Contribution.php
galette/templates/default/ajouter_contribution.tpl
galette/templates/default/gestion_adherents.tpl
galette/templates/default/mass_add_contribution.tpl [new file with mode: 0644]
galette/templates/default/mass_choose_type.tpl [new file with mode: 0644]

index 10ee7f96fd4730204dd5edc6d685e8601f88bc26..eab836cc0574f9a9d10da23031ece98695e8158e 100644 (file)
@@ -136,3 +136,18 @@ $app->post(
     '/document/{hash}',
     [PdfController::class, 'directlinkDocument']
 )->setName('get-directlink');
+
+$app->get(
+    '/contribution/mass-add/choose-type',
+    [Crud\ContributionsController::class, 'massAddChooseType']
+)->setName('massAddContributionsChooseType')->add($authenticate);
+
+$app->post(
+    '/contribution/mass-add',
+    [Crud\ContributionsController::class, 'massAddContributions']
+)->setName('massAddContributions')->add($authenticate);
+
+$app->post(
+    '/contribution/do-mass-add',
+    [Crud\ContributionsController::class, 'doMassAddContributions']
+)->setName('doMassAddContributions')->add($authenticate);
index 974346b2401b611915933dc9415f736784e2b1d0..fc7d0303e2bb0ed462bdf2b5bbcf0e77641b5794 100644 (file)
@@ -43,14 +43,9 @@ use Slim\Http\Response;
 use Galette\Entity\Adherent;
 use Galette\Entity\Contribution;
 use Galette\Entity\Transaction;
-use Galette\Repository\Contributions;
-use Galette\Repository\Transactions;
 use Galette\Repository\Members;
 use Galette\Entity\ContributionsTypes;
-use Galette\Core\GaletteMail;
-use Galette\IO\PdfMembersCards;
 use Galette\Repository\PaymentTypes;
-use Analog\Analog;
 
 /**
  * Galette contributions controller
@@ -92,13 +87,6 @@ class ContributionsController extends CrudController
         $ct = new ContributionsTypes($this->zdb);
         $contributions_types = $ct->getList($type === 'fee');
 
-        $disabled = array();
-
-        if (!is_int($contrib->id)) {
-            // initialiser la structure contribution à vide (nouvelle contribution)
-            $contribution['duree_mois_cotis'] = $this->preferences->pref_membership_ext;
-        }
-
         // template variable declaration
         $title = null;
         if ($type === 'fee') {
@@ -113,20 +101,9 @@ class ContributionsController extends CrudController
             $title .= ' (' . _T("creation") . ')';
         }
 
-        // required fields
-        $required = [
-            'id_type_cotis'     => 1,
-            'id_adh'            => 1,
-            'date_enreg'        => 1,
-            'date_debut_cotis'  => 1,
-            'date_fin_cotis'    => $contrib->isCotis(),
-            'montant_cotis'     => $contrib->isCotis() ? 1 : 0
-        ];
-
         $params = [
             'page_title'        => $title,
-            'required'          => $required,
-            'disabled'          => $disabled,
+            'required'          => $contrib->getRequired(),
             'contribution'      => $contrib,
             'adh_selected'      => $contrib->member,
             'type'              => $type
@@ -139,7 +116,7 @@ class ContributionsController extends CrudController
         $m = new Members();
         $members = $m->getSelectizedMembers(
             $this->zdb,
-            isset($contrib) && $contrib->member > 0 ? $contrib->member : null
+            $contrib->member > 0 ? $contrib->member : null
         );
 
         $params['members'] = [
@@ -152,7 +129,7 @@ class ContributionsController extends CrudController
         }
 
         $ext_membership = '';
-        if (isset($contrib) && $contrib->isCotis() || !isset($contrib) && $type === 'fee') {
+        if ($contrib->isCotis() || !isset($contrib) && $type === 'fee') {
             $ext_membership = $this->preferences->pref_membership_ext;
         }
         $params['pref_membership_ext'] = $ext_membership;
@@ -170,9 +147,9 @@ class ContributionsController extends CrudController
     /**
      * Add page
      *
-     * @param Request  $request  PSR Request
-     * @param Response $response PSR Response
-     * @param string   $type     Contribution type
+     * @param Request     $request  PSR Request
+     * @param Response    $response PSR Response
+     * @param string|null $type     Contribution type
      *
      * @return Response
      */
@@ -231,6 +208,157 @@ class ContributionsController extends CrudController
         return $this->store($request, $response, 'add', $type);
     }
 
+    /**
+     * Choose contribution type to mass add contribution
+     *
+     * @param Request  $request  PSR Request
+     * @param Response $response PSR Response
+     *
+     * @return Response
+     */
+    public function massAddChooseType(Request $request, Response $response): Response
+    {
+        $filters = $this->session->filter_members;
+        $data = [
+            'id'            => $filters->selected,
+            'redirect_uri'  => $this->router->pathFor('members')
+        ];
+
+        // display page
+        $this->view->render(
+            $response,
+            'mass_choose_type.tpl',
+            array(
+                'mode'          => $request->isXhr() ? 'ajax' : '',
+                'page_title'    => str_replace(
+                    '%count',
+                    count($data['id']),
+                    _T('Mass add contribution on %count members')
+                ),
+                'data'          => $data,
+                'form_url'      => $this->router->pathFor('massAddContributions'),
+                'cancel_uri'    => $this->router->pathFor('members')
+            )
+        );
+        return $response;
+    }
+
+    /**
+     * Massive change page
+     *
+     * @param Request  $request  PSR Request
+     * @param Response $response PSR Response
+     *
+     * @return Response
+     */
+    public function massAddContributions(Request $request, Response $response): Response
+    {
+        $post = $request->getParsedBody();
+        $filters = $this->session->filter_members;
+        $contribution = new Contribution($this->zdb, $this->login);
+
+        $type = $post['type'];
+        $data = [
+            'id'            => $filters->selected,
+            'redirect_uri'  => $this->router->pathFor('members'),
+            'type'          => $type
+        ];
+
+        // contribution types
+        $ct = new ContributionsTypes($this->zdb);
+        $contributions_types = $ct->getList($type === 'fee');
+
+        // display page
+        $this->view->render(
+            $response,
+            'mass_add_contribution.tpl',
+            array(
+                'mode'          => $request->isXhr() ? 'ajax' : '',
+                'page_title'    => str_replace(
+                    '%count',
+                    count($data['id']),
+                    _T('Mass add contribution on %count members')
+                ),
+                'form_url'      => $this->router->pathFor('doMassAddContributions'),
+                'cancel_uri'    => $this->router->pathFor('members'),
+                'data'          => $data,
+                'contribution'  => $contribution,
+                'type'          => $type,
+                'require_mass'  => true,
+                'required'      => $contribution->getRequired(),
+                'type_cotis_options' => $contributions_types
+            )
+        );
+        return $response;
+    }
+
+    /**
+     * Do massive contribution add
+     *
+     * @param Request  $request  PSR Request
+     * @param Response $response PSR Response
+     *
+     * @return Response
+     */
+    public function doMassAddContributions(Request $request, Response $response): Response
+    {
+        $post = $request->getParsedBody();
+        $members_ids = $post['id'];
+        unset($post['id']);
+
+        $error_detected = [];
+
+        // flagging required fields for first step only
+        $disabled = [];
+        $success = 0;
+        $errors = 0;
+
+        foreach ($members_ids as $member_id) {
+            $post[Adherent::PK] = (int)$member_id;
+            $contrib = new Contribution($this->zdb, $this->login);
+
+            // regular fields
+            $valid = $contrib->check($post, $contrib->getRequired(), $disabled);
+            if ($valid !== true) {
+                $error_detected = array_merge($error_detected, $valid);
+            }
+
+            //all goes well, we can proceed
+            if (count($error_detected) == 0) {
+                $store = $contrib->store();
+                if ($store === true) {
+                    ++$success;
+                    $files_res = $contrib->handleFiles($_FILES);
+                    if (is_array($files_res)) {
+                        $error_detected = array_merge($error_detected, $files_res);
+                    }
+                } else {
+                    ++$errors;
+                }
+            }
+        }
+
+        if (count($error_detected) == 0) {
+            $redirect_url = $this->router->pathFor('members');
+        } else {
+            //something went wrong.
+            //store entity in session
+            $redirect_url = $this->router->pathFor('massAddContributions');
+            //report errors
+            foreach ($error_detected as $error) {
+                $this->flash->addMessage(
+                    'error_detected',
+                    $error
+                );
+            }
+        }
+
+        //redirect to calling action
+        return $response
+            ->withStatus(301)
+            ->withHeader('Location', $redirect_url);
+    }
+
     // /CRUD - Create
     // CRUD - Read
 
@@ -549,19 +677,10 @@ class ContributionsController extends CrudController
             }
         }
 
-        // flagging required fields for first step only
-        $required = [
-            'id_type_cotis'     => 1,
-            'id_adh'            => 1,
-            'date_enreg'        => 1,
-            'montant_cotis'     => 1, //TODO: not always required, see #196
-            'date_debut_cotis'  => 1,
-            'date_fin_cotis'    => ($type === 'fee')
-        ];
         $disabled = [];
 
         // regular fields
-        $valid = $contrib->check($post, $required, $disabled);
+        $valid = $contrib->check($post, $contrib->getRequired(), $disabled);
         if ($valid !== true) {
             $error_detected = array_merge($error_detected, $valid);
         }
index b9ae3082439067167fe45215b5962084c90860d7..38ce68c609fa599544504f28e7d155e6123ac422 100644 (file)
@@ -1029,6 +1029,12 @@ class MembersController extends CrudController
                     ->withHeader('Location', $this->router->pathFor('masschangeMembers'));
             }
 
+            if (isset($post['masscontributions'])) {
+                return $response
+                    ->withStatus(301)
+                    ->withHeader('Location', $this->router->pathFor('massAddContributionsChooseType'));
+            }
+
             throw new \RuntimeException('Does not know what to batch :(');
         } else {
             $this->flash->addMessage(
index 000c0daf23bc85a576bc5bd85473c886fc6f4d9a..aa7a433f3aec05227475b4566685fdc6452f687c 100644 (file)
@@ -1365,4 +1365,23 @@ class Contribution
             return true;
         }
     }
+
+    /**
+     * Get required fields list
+     *
+     * @return array
+     */
+    public function getRequired(): array
+    {
+        // required fields
+        $required = [
+            'id_type_cotis'     => 1,
+            'id_adh'            => 1,
+            'date_enreg'        => 1,
+            'date_debut_cotis'  => 1,
+            'date_fin_cotis'    => $this->isCotis(),
+            'montant_cotis'     => $this->isCotis() ? 1 : 0
+        ];
+        return $required;
+    }
 }
index b7e2b84b1098b1b7ea5049a6f6303974d8f07b37..531bdb03dcc500d9afb3e8afd0749b5f01e78b47 100644 (file)
@@ -1,7 +1,12 @@
-{extends file="page.tpl"}
+{if isset($mode) && $mode eq 'ajax'}
+    {assign var="extend" value='ajax.tpl'}
+{else}
+    {assign var="extend" value='page.tpl'}
+{/if}
+{extends file=$extend}
 
 {block name="content"}
-{if isset($members.list)}
+{if isset($members.list) || $require_mass}
         <form action="{if $contribution->id}{path_for name="doEditContribution" data=["type" => $type, "id" => $contribution->id]}{else}{path_for name="doAddContribution" data=["type" => $type]}{/if}" enctype="multipart/form-data" method="post">
         <div class="bigtable">
     {if $contribution->isTransactionPart()}
                     </a>
     {/if}
 </legend>
+    {if !$require_mass}
                 <p>
                     <label for="id_adh" class="bline">{_T string="Contributor:"}</label>
-                    <select name="id_adh" id="id_adh" class="nochosen"{if isset($disabled.id_adh)} {$disabled.id_adh}{/if}>
+                    <select name="id_adh" id="id_adh" class="nochosen">
                         {if $adh_selected eq 0}
                         <option value="">{_T string="Search for name or ID and pick member"}</option>
                         {/if}
                         {/foreach}
                     </select>
                 </p>
+    {/if}
                 <p>
                     <label for="id_type_cotis" class="bline">{_T string="Contribution type:"}</label>
-                    <select name="id_type_cotis" id="id_type_cotis"{if $required.id_type_cotis eq 1} required="required"{/if}>
+                    <select name="id_type_cotis" id="id_type_cotis"{if isset($required.id_type_cotis) &&  ($required.id_type_cotis eq 1)} required="required"{/if}>
                         {if $contribution->type}
                             {assign var="selectedid" value=$contribution->type->id}
                         {else}
                 <legend class="ui-state-active ui-corner-top">{if $type eq "fee"}{_T string="Details of membership fee"}{else}{_T string="Details of donation"}{/if}</legend>
                 <p>
                     <label class="bline" for="montant_cotis">{_T string="Amount:"}</label>
-                    <input type="text" name="montant_cotis" id="montant_cotis" value="{$contribution->amount}" maxlength="10"{if $required.montant_cotis eq 1} required="required"{/if}/>
+                    <input type="text" name="montant_cotis" id="montant_cotis" value="{$contribution->amount}" maxlength="10"{if isset($required.montant_cotis) &&  ($required.montant_cotis eq 1)} required="required"{/if}/>
                 </p>
                 {* payment type *}
                 {assign var="ptype" value=$contribution->payment_type}
                     <label class="bline" for="date_enreg">
                         {_T string="Record date:"}
                     </label>
-                    <input class="past-date-pick" type="text" name="date_enreg" id="date_enreg" value="{$contribution->date}" maxlength="10"{if $required.date_enreg eq 1} required="required"{/if}/>
+                    <input class="past-date-pick" type="text" name="date_enreg" id="date_enreg" value="{$contribution->date}" maxlength="10"{if isset($required.date_enreg) &&  ($required.date_enreg eq 1)} required="required"{/if}/>
                     <span class="exemple">{_T string="(yyyy-mm-dd format)"}</span>
                 </p>
 
                             {_T string="Date of contribution:"}
                         {/if}
                     </label>
-                    <input class="past-date-pick" type="text" name="date_debut_cotis" id="date_debut_cotis" value="{$contribution->begin_date}" maxlength="10"{if $required.date_debut_cotis eq 1} required="required"{/if}/>
+                    <input class="past-date-pick" type="text" name="date_debut_cotis" id="date_debut_cotis" value="{$contribution->begin_date}" maxlength="10"{if isset($required.date_debut_cotis) &&  ($required.date_debut_cotis eq 1)} required="required"{/if}/>
                     <span class="exemple">{_T string="(yyyy-mm-dd format)"}</span>
                 </p>
         {if $type eq "fee"}
                 <p>
-            {if $pref_membership_ext != ""}
+            {if $preferences->pref_membership_ext != ""}
                     <label class="bline" for="duree_mois_cotis">{_T string="Membership extension:"}</label>
-                    <input type="text" name="duree_mois_cotis" id="duree_mois_cotis" value="{$contribution->duration}" maxlength="3"{if $required.date_fin_cotis eq 1} required="required"{/if}/>
+                    <input type="text" name="duree_mois_cotis" id="duree_mois_cotis" value="{$contribution->duration}" maxlength="3"{if isset($required.date_fin_cotis) &&  ($required.date_fin_cotis eq 1)} required="required"{/if}/>
                     <span class="exemple">{_T string="months"}</span>
             {else}
                     <label class="bline" for="date_fin_cotis">{_T string="End date of membership:"}</label>
-                    <input type="text" name="date_fin_cotis" id="date_fin_cotis" value="{$contribution->end_date}" maxlength="10"{if $required.date_fin_cotis eq 1} required="required"{/if}/>
+                    <input type="text" name="date_fin_cotis" id="date_fin_cotis" value="{$contribution->end_date}" maxlength="10"{if isset($required.date_fin_cotis) &&  ($required.date_fin_cotis eq 1)} required="required"{/if}/>
                     <span class="exemple">{_T string="(yyyy-mm-dd format)"}</span>
             {/if}
                 </p>
     {/if}
         {include file="edit_dynamic_fields.tpl" object=$contribution}
     {if not $contribution->id and $pref_mail_method neq constant('Galette\Core\GaletteMail::METHOD_DISABLED')}
+        {if !$require_mass}
             <p>
                 <label for="mail_confirm">{_T string="Notify member"}</label>
                 <input type="checkbox" name="mail_confirm" id="mail_confirm" value="1" {if $preferences->pref_bool_mailowner || isset($smarty.post.mail_confirm) and $smarty.post.mail_confirm != ""}checked="checked"{/if}/>
                 <br/><span class="exemple">{_T string="Member will receive a notification by email, if he has an address."}</span>
             </p>
+        {/if}
     {/if}
         </div>
+    {if !$require_mass}
         <div class="button-container">
             <button type="submit" name="valid" class="action">
                 <i class="fas fa-save fa-fw"></i> {_T string="Save"}
             <input type="hidden" name="valid" value="1"/>
             <input type="hidden" name="trans_id" value="{if $contribution->transaction neq NULL}{$contribution->transaction->id}{/if}"/>
         </div>
+    {/if}
         </form>
 {else} {* No members *}
     <div class="center" id="warningbox">
index 28f6ad7025f890ee95f0f8684bba66915bf130ff..6cdb3345ea6bf0ef3b3a17da7e1908609f1650e2 100644 (file)
@@ -308,6 +308,11 @@ We have to use a template file, so Smarty will do its work (like replacing varia
                     <i class="fas fa-user-edit fa-fw"></i> {_T string="Mass change"}
                 </button>
             </li>
+            <li>
+                <button type="submit" id="masscontributions" name="masscontributions" class="action">
+                    <i class="fas fa-cookie-bite fa-fw"></i> {_T string="Mass add contributions"}
+                </button>
+            </li>
         {if $pref_mail_method neq constant('Galette\Core\GaletteMail::METHOD_DISABLED')}
             <li>
                 <button type="submit" id="sendmail" name="mailing">
@@ -461,9 +466,11 @@ We have to use a template file, so Smarty will do its work (like replacing varia
             _bind_check();
             _bind_legend();
 
-            $('.selection_menu *[type="submit"], .selection_menu *[type="button"]').click(function(){
-                if ( this.id == 'delete' ) {
+            $('.selection_menu *[type="submit"], .selection_menu *[type="button"]').click(function(event){
+                event.preventDefault();
+                if ( this.id == 'delete' || this.id == 'masschange' ) {
                     //mass removal is handled from 2 steps removal
+                    //mass change is specifically handled below
                     return;
                 }
 
@@ -503,6 +510,42 @@ We have to use a template file, so Smarty will do its work (like replacing varia
                         _attendance_sheet_details();
                         return false;
                     }
+
+                    if (this.id == 'masscontributions') {
+                        $.ajax({
+                            url: '{path_for name="batch-memberslist"}',
+                            type: "POST",
+                            data: {
+                                ajax: true,
+                                masscontributions: true,
+                                member_sel: $('#listform input[type=\"checkbox\"]:checked').map(function(){
+                                    return $(this).val();
+                                }).get()
+                            },
+                            datatype: 'json',
+                            {include file="js_loader.tpl"},
+                            success: function(res){
+                                var _res = $(res);
+                                _bindmassres(_res);
+                                $('body').append(_res);
+
+                                _initTooltips('#mass_contributions');
+                                _massCheckboxes('#mass_contributions');
+
+                                _res.dialog({
+                                    width: 'auto',
+                                    modal: true,
+                                    close: function(event, ui){
+                                        $(this).dialog('destroy').remove()
+                                    }
+                                });
+                            },
+                            error: function() {
+                                alert("{_T string="An error occurred :(" escape="js"}");
+                            }
+                        });
+                    }
+
                     return true;
                 }
             });
@@ -572,6 +615,10 @@ We have to use a template file, so Smarty will do its work (like replacing varia
 
             res.find('input[type=submit]')
                 .button();
+
+            res.find('select:not(.nochosen)').selectize({
+                maxItems: 1
+            });
         }
 
         $('#masschange').off('click').on('click', function(event) {
diff --git a/galette/templates/default/mass_add_contribution.tpl b/galette/templates/default/mass_add_contribution.tpl
new file mode 100644 (file)
index 0000000..6e05351
--- /dev/null
@@ -0,0 +1,24 @@
+{extends file='ajouter_contribution.tpl'}
+{block name="content"}
+    <div id="mass_contributions"{if $mode neq 'ajax'} class="center"{else} title="{$page_title}"{/if}>
+    <form action="{$form_url}" method="post">
+        {if $mode neq 'ajax'}<h2>{$page_title}</h2>{/if}
+        <div class="button-container">
+            {$smarty.block.parent}
+            <input type="submit" id="masschange" class="button" value="{if !isset($changes)}{_T string="Edit"}{else}{_T string="OK"}{/if}"/>
+            <a href="{$cancel_uri}" class="button" id="btncancel">{_T string="Cancel"}</a>
+            <input type="hidden" name="confirm" value="1"/>
+            {if $mode eq 'ajax'}<input type="hidden" name="ajax" value="true"/>{/if}
+            {foreach $data as $key=>$value}
+                {if is_array($value)}
+                    {foreach $value as $val}
+                <input type="hidden" name="{$key}[]" value="{$val}"/>
+                    {/foreach}
+                {else}
+                <input type="hidden" name="{$key}" value="{$value}"/>
+                {/if}
+            {/foreach}
+        </div>
+    </form>
+    </div>
+{/block}
diff --git a/galette/templates/default/mass_choose_type.tpl b/galette/templates/default/mass_choose_type.tpl
new file mode 100644 (file)
index 0000000..89b131e
--- /dev/null
@@ -0,0 +1,33 @@
+{if isset($mode) && $mode eq 'ajax'}
+    {assign var="extend" value='ajax.tpl'}
+{else}
+    {assign var="extend" value='page.tpl'}
+{/if}
+{extends file=$extend}
+
+{block name="content"}
+    <div id="mass_contributions"{if $mode neq 'ajax'} class="center"{else} title="{$page_title}"{/if}>
+    <form action="{$form_url}" method="post">
+        {if $mode neq 'ajax'}<h2>{$page_title}</h2>{/if}
+        <label for="type">{_T string="Contribution type"}</label>
+        <select name="type" id="type">
+            <option value="{constant('Galette\Entity\Contribution::TYPE_FEE')}">{_T string="Membership"}</option>
+            <option value="{constant('Galette\Entity\Contribution::TYPE_DONATION')}">{_T string="Donation"}</option>
+        </select>
+        <div class="button-container">
+            <input type="submit" id="masschange" class="button" value="{_T string="OK"}"/>
+            <a href="{$cancel_uri}" class="button" id="btncancel">{_T string="Cancel"}</a>
+            {if $mode eq 'ajax'}<input type="hidden" name="ajax" value="true"/>{/if}
+            {foreach $data as $key=>$value}
+                {if is_array($value)}
+                    {foreach $value as $val}
+                <input type="hidden" name="{$key}[]" value="{$val}"/>
+                    {/foreach}
+                {else}
+                <input type="hidden" name="{$key}" value="{$value}"/>
+                {/if}
+            {/foreach}
+        </div>
+    </form>
+    </div>
+{/block}