From 3a7ce22a0c646741321efbd7d636c93990566e20 Mon Sep 17 00:00:00 2001 From: olayiwola-compucorp Date: Tue, 25 Jul 2023 08:58:44 +0100 Subject: [PATCH] CIWEMB-320: User can optionally record payment for new membership --- .../Hook/BuildForm/MembershipCreate.php | 70 +++++++++ .../Hook/ValidateForm/MembershipCreate.php | 56 +++++++ financeextras.php | 2 + js/modifyMemberForm.js | 146 ++++++++++++++++++ .../Financeextras/Form/Member/AddPayment.tpl | 34 ++++ 5 files changed, 308 insertions(+) create mode 100644 Civi/Financeextras/Hook/BuildForm/MembershipCreate.php create mode 100644 Civi/Financeextras/Hook/ValidateForm/MembershipCreate.php create mode 100644 js/modifyMemberForm.js create mode 100644 templates/CRM/Financeextras/Form/Member/AddPayment.tpl diff --git a/Civi/Financeextras/Hook/BuildForm/MembershipCreate.php b/Civi/Financeextras/Hook/BuildForm/MembershipCreate.php new file mode 100644 index 00000000..0b1f2890 --- /dev/null +++ b/Civi/Financeextras/Hook/BuildForm/MembershipCreate.php @@ -0,0 +1,70 @@ +configureRecordContributionField(); + } + + /** + * CiviCRM by default always shows the payment fields for new contribution, + * This method configures the display of these payment related fields by + * grouping them together as a block and adds a 'Record Payment' checkbox + * that determines visibility of this block. + * + * Also, adds a payment amount field, that allows users to specify the amount + * they would like to record for the contribution. + */ + private function configureRecordContributionField() { + if (!$this->isEdit()) { + $this->form->addElement('radio', 'fe_member_type', NULL, ts('Paid Membership'), 'paid_member'); + $this->form->addElement('radio', 'fe_member_type', NULL, ts('Free Membership'), 'free_member'); + $this->form->add('checkbox', 'fe_record_payment_check', ts('Record Payment'), NULL); + $this->form->add('text', 'fe_record_payment_amount', ts('Amount'), ['readonly' => TRUE]); + \Civi::resources()->add([ + 'scriptFile' => [E::LONG_NAME, 'js/modifyMemberForm.js'], + 'region' => 'page-header', + ]); + \Civi::resources()->add([ + 'template' => 'CRM/Financeextras/Form/Member/AddPayment.tpl', + 'region' => 'page-body', + ]); + \Civi::resources()->addVars('financeextras', ['currencySymbol' => $this->getCurrencySymbol()]); + } + } + + /** + * @return string + */ + private function getCurrencySymbol() { + $config = \CRM_Core_Config::singleton(); + return \CRM_Core_DAO::getFieldValue('CRM_Financial_DAO_Currency', $config->defaultCurrency, 'symbol', 'name'); + } + + private function isEdit() { + return !empty($this->form->_id); + } + + /** + * Checks if the hook should run. + * + * @param \CRM_Core_Form $form + * @param string $formName + * + * @return bool + */ + public static function shouldHandle($form, $formName) { + return $formName === "CRM_Member_Form_Membership"; + } + +} diff --git a/Civi/Financeextras/Hook/ValidateForm/MembershipCreate.php b/Civi/Financeextras/Hook/ValidateForm/MembershipCreate.php new file mode 100644 index 00000000..d3290ca4 --- /dev/null +++ b/Civi/Financeextras/Hook/ValidateForm/MembershipCreate.php @@ -0,0 +1,56 @@ +validatePaymentForm(); + } + + public function validatePaymentForm() { + if (empty($this->fields['record_contribution'])) { + // Don't do anything if user will not be recording contribution + return; + } + + if ($this->fields['contribution_type_toggle'] == 'payment_plan') { + // Don't do anything if membership is paid for using the payment plan option + return; + } + + if (empty($this->fields['fe_record_payment_check'])) { + $data = &$this->form->controller->container(); + // Before the form is submitted, we ensure that if the "Record Payment" box is unchecked, + // payment will not be recorded against the contribution. + $data['values']['Membership']['contribution_status_id'] = OptionValueUtils::getValueForOptionValue('contribution_status', 'Pending'); + } + + if (!empty($this->fields['fe_record_payment_check']) && empty($this->fields['fe_record_payment_amount'])) { + $this->errors['fe_record_payment_amount'] = ts('Payment amount is required'); + } + } + + /** + * Checks if the hook should run. + * + * @param CRM_Core_Form $form + * @param string $formName + * + * @return bool + */ + public static function shouldHandle($form, $formName) { + return $formName === "CRM_Member_Form_Membership" && ($form->_action & \CRM_Core_Action::ADD); + } + +} diff --git a/financeextras.php b/financeextras.php index c1025cbc..f12a3150 100644 --- a/financeextras.php +++ b/financeextras.php @@ -146,6 +146,7 @@ function financeextras_civicrm_post($op, $objectName, $objectId, &$objectRef) { */ function financeextras_civicrm_validateForm($formName, &$fields, &$files, &$form, &$errors) { $hooks = [ + \Civi\Financeextras\Hook\ValidateForm\MembershipCreate::class, \Civi\Financeextras\Hook\ValidateForm\ContributionCreate::class, ]; @@ -176,6 +177,7 @@ function financeextras_civicrm_postProcess($formName, $form) { */ function financeextras_civicrm_buildForm($formName, &$form) { $hooks = [ + \Civi\Financeextras\Hook\BuildForm\MembershipCreate::class, \Civi\Financeextras\Hook\BuildForm\ContributionCreate::class, ]; diff --git a/js/modifyMemberForm.js b/js/modifyMemberForm.js new file mode 100644 index 00000000..b59b64e4 --- /dev/null +++ b/js/modifyMemberForm.js @@ -0,0 +1,146 @@ +CRM.$(function ($) { + + (function() { + setTotalAmount(); + togglePaymentBlock(); + toggleMembershipType(); + toggleContributionBlock(); + placePaymentFieldsTogether(); + hidePaymentFieldsOnPaymentToggler(); + })(); + + function setTotalAmount() { + const recordPaymentAmount = document.querySelector("input[name=fe_record_payment_amount]"); + observeElement('input[name=total_amount]', "value", function () { + toggleMembershipType(); + recordPaymentAmount.value = $('#total_amount').val(); + }); + } + + function togglePaymentBlock() { + $('input[name=fe_record_payment_check]').prop("checked", true).trigger('change') + + $('input[name=fe_record_payment_check]').on('change', () => { + const recordPayment = $('input[name=fe_record_payment_check]').is(':checked') + $('tr.record_payment-block_row').toggle(recordPayment) + }); + } + + function toggleMembershipType() { + if ($('input[name=record_contribution]').is(':checked')) { + $('input[name=fe_member_type][value=paid_member]').prop("checked", true) + } else { + $('input[name=fe_member_type][value=free_member]').prop("checked", true) + } + } + + function toggleContributionBlock() { + toggleMembershipType(); + + $('tr#contri').after( + $('').addClass('fe-membership_type-row').append($('').attr('colspan', 2).append( + $('.fe-membership_type') + )) + ); + + $('input:radio[name=fe_member_type]').on('change', () => { + const isPaid = $('input:radio[name=fe_member_type][value=paid_member]').is(':checked'); + $('input#record_contribution').prop("checked", !isPaid).trigger('click') + }); + + $('tr#contri').hide(); + } + + function placePaymentFieldsTogether() { + $('tr.crm-membership-form-block-receive_date').after( + $('').addClass('record_payment-block_row').append($('').attr('colspan', 2).append( + $('.record_payment-block') + )) + ) + + waitForElement($, 'input[name=contribution_type_toggle]', + () => { + $('tr.crm-membership-form-block-receive_date').before($('tr.crm-membership-form-block-financial_type_id')); + $('tr.crm-contribution-form-block-financeextras_record_payment_amount').after( + $('tr.crm-membership-form-block-payment_instrument_id') + ); + $('tr.crm-membership-form-block-payment_instrument_id').after($('tr.crm-membership-form-block-trxn_id')) + $('tr.crm-membership-form-block-trxn_id').after($('tr.crm-membership-form-block-billing')) + + $('tr.crm-membership-form-block-contribution_status_id').hide() + } + ); + + $('tr.record_payment-block_row').before( + $('').append($('').attr('colspan', 2).append( + $('.record_payment-block_check') + )) + ) + + const symbol = CRM.vars.financeextras.currencySymbol; + $('.crm-membership-form-block-total_amount label').text('Contribution Total Amount') + $('.crm-membership-form-block-financial_type_id label').text('Contribution Financial Type') + $('#total_amount').before($('').text(`${symbol} `)) + $('.record_payment-block #currency-symbol').text(symbol) + } + + function hidePaymentFieldsOnPaymentToggler() { + $('li[data-selector="payment_plan"]').click( () => { + $('tr.record_payment-block_row').hide() + $('div.record_payment-block_check').hide(); + }); + + $('li[data-selector="contribution"]').click( () => { + $('div.record_payment-block_check').show() + togglePaymentBlock(); + }); + } + + /** + * Triggers callback when element attribute changes. + * + * @param {object} $ + * @param {string} elementPath + * @param {object} callBack + */ + function waitForElement($, elementPath, callBack) { + (new MutationObserver(function() { + callBack($(elementPath)); + })).observe(document.querySelector(elementPath), { + attributes: true, + }); + } + + /** + * Observes change in property value for an element. + * + * This method is used to listen for change in input fields + * that doesn't emit a change event when their value changes. + * + * @param {string} elementPath + * @param {string} property + * @param {function} callback + * @param {number} delay + */ + function observeElement(elementPath, property, callback, delay = 0) { + const element = document.querySelector(elementPath) + const elementPrototype = Object.getPrototypeOf(element); + if (Object.hasOwn(elementPrototype, property)) { + const descriptor = Object.getOwnPropertyDescriptor(elementPrototype, property); + Object.defineProperty(element, property, { + get: function() { + return descriptor.get.apply(this, arguments); + }, + set: function () { + const oldValue = this[property]; + descriptor.set.apply(this, arguments); + const newValue = this[property]; + if (typeof callback == "function") { + setTimeout(callback.bind(this, oldValue, newValue), delay); + } + } + }); + } +} + +}); diff --git a/templates/CRM/Financeextras/Form/Member/AddPayment.tpl b/templates/CRM/Financeextras/Form/Member/AddPayment.tpl new file mode 100644 index 00000000..ffb0a1bf --- /dev/null +++ b/templates/CRM/Financeextras/Form/Member/AddPayment.tpl @@ -0,0 +1,34 @@ +
+ + + + + + + + + + + +
{$form.fe_member_type.paid_member.label}{$form.fe_member_type.paid_member.html}
{$form.fe_member_type.free_member.label}{$form.fe_member_type.free_member.html}
+
+
+ + + + + + + +
{$form.fe_record_payment_check.html}{$form.fe_record_payment_check.label}
+
+
+ + + + + + + +
{$form.fe_record_payment_amount.label} * {$form.fe_record_payment_amount.html}
+