diff --git a/my_compassion/__manifest__.py b/my_compassion/__manifest__.py index 115ad51c1..459206a31 100644 --- a/my_compassion/__manifest__.py +++ b/my_compassion/__manifest__.py @@ -91,6 +91,9 @@ "templates/components/my2_giving_limits_modal.xml", "templates/components/my2_checkout.xml", "templates/components/my2_weather_time_container.xml", + "templates/components/my2_sponsorships_section.xml", + "templates/components/my2_payment_method_modal.xml", + "templates/components/my2_payment_method_card.xml", # Other data the depends on the templates "data/my2_new_sponsorship_wizard_steps.xml", ], diff --git a/my_compassion/controllers/my2_donations.py b/my_compassion/controllers/my2_donations.py index 1eb723638..9a5c31033 100644 --- a/my_compassion/controllers/my2_donations.py +++ b/my_compassion/controllers/my2_donations.py @@ -8,13 +8,15 @@ # ############################################################################## +import json import math from collections import defaultdict from datetime import datetime, timedelta from werkzeug.exceptions import BadRequest, NotFound -from odoo import fields, http +import odoo +from odoo import _, fields, http from odoo.http import request from odoo.addons.portal.controllers.portal import CustomerPortal @@ -397,7 +399,14 @@ def my_donations(self, invoice_page=1, invoice_per_page=12, **kw): partner = request.env.user.partner_id # Active sponsorships - active_sponsorships = partner.get_portal_sponsorships("active") + active_sponsorships = partner.get_portal_sponsorships(["active", "mandate"]) + + # Group sponsorships by their backend Contract Group + sponsorship_groups = active_sponsorships.mapped("group_id") + + # Put all payment methods into an array + all_groups = partner.get_payment_modes() + payment_methods = [group.get_payment_method_info() for group in all_groups] # Due invoices date_filter_up_bound = datetime.today() + timedelta(days=30) @@ -417,16 +426,16 @@ def my_donations(self, invoice_page=1, invoice_per_page=12, **kw): ) ) - # Computing the total price of the active sponsorships grouped - # per sponsorship frequency and payment method. - # group_id groups the invoices that have the same payment method and frequency. + # Total cost calculation tot_cost_per_frequency = defaultdict(lambda: defaultdict(float)) for sponsorship in active_sponsorships: currency = sponsorship.pricelist_id.currency_id.name - tot_cost_per_frequency[sponsorship.group_id.month_interval][ - currency - ] += sponsorship.total_amount + # Ensure group exists + if sponsorship.group_id: + tot_cost_per_frequency[sponsorship.group_id.month_interval][ + currency + ] += sponsorship.total_amount paid_invoices_data = self._get_paginated_paid_invoices( partner, invoice_page, invoice_per_page @@ -436,6 +445,9 @@ def my_donations(self, invoice_page=1, invoice_per_page=12, **kw): values.update( { "active_sponsorships": active_sponsorships, + "sponsorship_groups": sponsorship_groups, + "payment_methods": payment_methods, + "payment_methods_json": json.dumps(payment_methods), "tot_cost_per_frequency": tot_cost_per_frequency, "due_invoices": due_invoices, "paid_invoices_subset": paid_invoices_data["paid_invoices_subset"], @@ -466,6 +478,240 @@ def my_donations_history(self, invoice_page=1, invoice_per_page=12, **kw): return {"html": html} + @http.route( + "/my2/donations/get_payment_methods_sponsor", + type="json", + auth="user", + website=True, + ) + def get_payment_methods_sponsor(self, **kwargs): + """ + Returns a list of payment methods (saved tokens and acquirers) for the current + user. + """ + partner = request.env.user.partner_id + groups = partner.get_payment_modes() + payment_methods = [] + + for group in groups: + info = group.get_payment_method_info() + if not info: + continue + method = dict(info) + method["group_id"] = group.id + + payment_methods.append(method) + + return payment_methods + + @http.route( + "/my2/donation/change_method_contract", type="json", auth="user", website=True + ) + def change_payment_method_contract(self, contract_id, group_id, **kwargs): + """ + Changes the payment method for a specific contract. + + :param contract_id: ID of the recurring.contract to update. + :param group_id: ID of an existing group to merge into + """ + partner = request.env.user.partner_id + if not contract_id or not group_id: + raise BadRequest() + # Verify that the contract belongs to the user + contract = ( + request.env["recurring.contract"] + .sudo() + .search([("id", "=", int(contract_id)), ("partner_id", "=", partner.id)]) + ) + if not contract: + raise NotFound() + + success = contract.change_contract_group(int(group_id)) + if success: + # Render the updated list + values = self._prepare_sponsorship_values(partner) + html = request.env["ir.qweb"]._render( + "my_compassion.my2_sponsorships_section", values + ) + return { + "success": True, + "html": html, + "payment_methods": values["payment_methods"], + } + + return {"success": False, "error": _("Operation failed")} + + @http.route( + "/my2/donation/change_method_group", type="json", auth="user", website=True + ) + def change_payment_method_group( + self, group_id, new_group_id=None, new_bvr_ref=None, **kwargs + ): + """ + Endpoint to update payment method for a sponsorship group. + Accepts new_group_id (to merge) or new_bvr_ref (to update ref). + """ + partner = request.env.user.partner_id + + if not group_id: + raise BadRequest(_("Group ID is required.")) + + # Security Check: Search ensures the group belongs to the logged-in user + group = ( + request.env["recurring.contract.group"] + .sudo() + .search( + [("id", "=", int(group_id)), ("partner_id", "=", partner.id)], limit=1 + ) + ) + + if not group: + raise NotFound(_("Payment group not found or access denied.")) + + # Call the model method to perform the logic + success = group.change_payment_method( + new_group_id=new_group_id, new_bvr_ref=new_bvr_ref + ) + + if success: + values = self._prepare_sponsorship_values(partner) + html = request.env["ir.qweb"]._render( + "my_compassion.my2_sponsorships_section", values + ) + return { + "success": True, + "html": html, + "payment_methods": values["payment_methods"], + } + + return {"success": False, "error": _("Operation failed")} + + # Configuration for supported manual payment methods + # Key: frontend 'value' from the select input + # Value: 'name' (or partial name) to search for in account.payment.mode + _payment_mode_map = { + "permanent_order": "Permanent Order", + "bvr": "BVR", + } + + @http.route( + "/my2/donation/add_payment_method_group", type="json", auth="user", website=True + ) + def add_payment_method_group( + self, + recurring_unit="month", + method_type="bvr", + advance_billing_months=1, + **kwargs, + ): + """ + Creates a new Contract Group with manual BVR/Permanent Order details. + """ + partner = request.env.user.partner_id + + # 1. Resolve Payment Mode Search Term + mode_search_term = self._payment_mode_map.get(method_type) + + if not mode_search_term: + return { + "success": False, + "error": _("Invalid payment method type selected."), + } + + # 2. Find the Payment Mode + # Exact match attempt + payment_mode = ( + request.env["account.payment.mode"] + .sudo() + .search([("name", "=", mode_search_term)], limit=1) + ) + + # Fallback: Loose search (ilike) if exact match fails + if not payment_mode: + payment_mode = ( + request.env["account.payment.mode"] + .sudo() + .search([("name", "ilike", mode_search_term)], limit=1) + ) + + if not payment_mode: + return { + "success": False, + "error": _('Configuration Error: Payment mode "%s" not found.') + % mode_search_term, + } + + # 3. Create the Group + try: + new_group = ( + request.env["recurring.contract.group"] + .sudo() + .create( + { + "partner_id": partner.id, + "payment_mode_id": payment_mode.id, + "recurring_unit": recurring_unit, + "recurring_value": int(advance_billing_months), + "active": True, + } + ) + ) + new_bvr_ref = new_group.compute_partner_bvr_ref(partner) + if new_bvr_ref: + new_group.bvr_reference = new_bvr_ref + + if new_group: + values = self._prepare_sponsorship_values(request.env.user.partner_id) + html = request.env["ir.qweb"]._render( + "my_compassion.my2_sponsorships_section", values + ) + return { + "success": True, + "html": html, + "group_id": new_group.id, + "payment_methods": values["payment_methods"], + } + + return {"success": False} + + except odoo.exceptions.ValidationError: + return { + "success": False, + "error": _("An unexpected error occurred. Please try again."), + } + + def _prepare_sponsorship_values(self, partner): + """ + Helper to fetch all data required for the sponsorship list view. + Returns a dict of values for QWeb rendering. + """ + # 1. Fetch Active Sponsorships + active_sponsorships = partner.get_portal_sponsorships(["active", "mandate"]) + + # 2. Fetch Groups + sponsorship_groups = active_sponsorships.mapped("group_id") + + # 3. Calculate Totals + tot_cost_per_frequency = defaultdict(lambda: defaultdict(float)) + for sponsorship in active_sponsorships: + currency = sponsorship.pricelist_id.currency_id.name + if sponsorship.group_id: + tot_cost_per_frequency[sponsorship.group_id.month_interval][ + currency + ] += sponsorship.total_amount + + # 4. Fetch Available Methods (for modals) + all_groups = partner.get_payment_modes() + payment_methods = [group.get_payment_method_info() for group in all_groups] + + return { + "active_sponsorships": active_sponsorships, + "sponsorship_groups": sponsorship_groups, + "tot_cost_per_frequency": tot_cost_per_frequency, + "payment_methods": payment_methods, + "payment_methods_json": json.dumps(payment_methods), + } + def _get_paginated_paid_invoices( self, partner, invoice_page=1, invoice_per_page=12 ): diff --git a/my_compassion/data/payment_options.xml b/my_compassion/data/payment_options.xml new file mode 100644 index 000000000..267d50b95 --- /dev/null +++ b/my_compassion/data/payment_options.xml @@ -0,0 +1,18 @@ + + + + + eBill + + variable + + + + + + eBill + ebill + inbound + + + diff --git a/my_compassion/models/contract_group.py b/my_compassion/models/contract_group.py index 39ca65c25..86bb95d59 100644 --- a/my_compassion/models/contract_group.py +++ b/my_compassion/models/contract_group.py @@ -6,16 +6,31 @@ # The licence is in the file __manifest__.py # ############################################################################## -from odoo import fields, models +from odoo import _, api, fields, models class ContractGroup(models.Model): _name = "recurring.contract.group" _inherit = ["recurring.contract.group", "translatable.model"] - gender = fields.Selection(store=False) + active = fields.Boolean(default=True) + payment_token_id = fields.Many2one("payment.token", string="Payment Token") + gender = fields.Selection(related="partner_id.gender", store=True, readonly=False) total_amount = fields.Float(compute="_compute_total_amount") + active_contract_count = fields.Integer( + string="Active Contracts Count", compute="_compute_active_contract_count" + ) + + @api.depends("contract_ids.state") + def _compute_active_contract_count(self): + for group in self: + group.active_contract_count = len( + group.contract_ids.filtered( + lambda s: s.state not in ["terminated", "cancelled"] + ) + ) + def _compute_total_amount(self): for group in self: group.total_amount = sum( @@ -23,3 +38,71 @@ def _compute_total_amount(self): lambda s: s.state not in ["terminated", "cancelled"] ).mapped("total_amount") ) + + def get_payment_method_info(self): + """ + Returns a dict containing display info for the group's payment method. + Used in MyCompassion2.0 portal. + """ + self.ensure_one() + + # Default / Fallback values + info = { + "icon": False, + "ref_number": False, + "label": _("Unknown Method"), + "expire_date": False, + "is_card": False, + "mode_id": self.payment_mode_id.id if self.payment_mode_id else False, + "group_id": self.id, + } + + if not self.payment_mode_id: + return info + + all_icons = self.env["payment.icon"].sudo().search([("image", "!=", False)]) + for icon in all_icons: + if icon.name.lower() in self.payment_mode_id.name.lower(): + info["icon"] = icon.id + break + + # Basic Mode Info + info["label"] = self.payment_mode_id.display_name + info["type"] = "mode" + info["ref_number"] = self.bvr_reference if self.bvr_reference else False + + return info + + def change_payment_method(self, new_group_id=None, new_bvr_ref=None): + """ + Update the contract group by either merging into an existing group + (if new_group_id provided) or finding/creating a group for a specific + payment mode (if payment_mode_id provided). + """ + self.ensure_one() + + # Merge into another Payment Group + if new_group_id: + target_group = self.env["recurring.contract.group"].browse( + int(new_group_id) + ) + + # Validation: Target must exist and belong to the same partner + if not target_group.exists() or target_group.partner_id != self.partner_id: + return False + + # Avoid self-merge + if target_group.id == self.id: + return True + + # Move all contracts to the target group + self.active_contract_ids.write({"group_id": target_group.id}) + return True + + # Update Reference (e.g. manual BVR or LSV reference update) + if new_bvr_ref is not None: + # Updating the reference for the current group + self.write({"bvr_reference": new_bvr_ref}) + return True + + return False diff --git a/my_compassion/models/contracts.py b/my_compassion/models/contracts.py index 7fc5173bd..36635e1a9 100644 --- a/my_compassion/models/contracts.py +++ b/my_compassion/models/contracts.py @@ -13,5 +13,30 @@ def _compute_can_show_on_my_compassion(self): for contract in self: contract.can_show_on_my_compassion = contract.state in [ "active", + "mandate", "terminated", ] or (contract.state != "cancelled" and not contract.parent_id) + + def change_contract_group(self, new_group_id): + """ + Moves the sponsorship (self) to the specified contract group. + :param new_group_id: int ID of the target recurring.contract.group + """ + self.ensure_one() + + if not new_group_id: + return False + + # If we are already in this group, do nothing + if self.group_id.id == new_group_id: + return True + + target_group = self.env["recurring.contract.group"].browse(new_group_id) + + if not target_group.exists() or target_group.partner_id != self.partner_id: + return False + + # Move the contract to the new group + self.write({"group_id": target_group.id}) + + return True diff --git a/my_compassion/models/res_partner.py b/my_compassion/models/res_partner.py index f39b504a2..66576d19b 100644 --- a/my_compassion/models/res_partner.py +++ b/my_compassion/models/res_partner.py @@ -53,7 +53,7 @@ def _compute_is_sponsor(self): "|", ("partner_id", "=", partner.id), ("correspondent_id", "=", partner.id), - ("state", "in", ["waiting", "active"]), + ("state", "in", ["waiting", "mandate", "active"]), ("child_id", "!=", False), ], ) @@ -93,3 +93,19 @@ def _compute_is_donor(self): donor_ids = {data["partner_id"][0] for data in donors_data} for partner in self: partner.is_donor = partner.id in donor_ids + + def get_payment_modes(self): + """ + Retrieve all unique payment modes currently linked to the partner. + Used to display existing methods. + """ + self.ensure_one() + # Find groups for this partner that have a payment mode. + groups = self.env["recurring.contract.group"].search( + [ + ("partner_id", "=", self.id), + ("payment_mode_id", "!=", False), + ] + ) + + return groups diff --git a/my_compassion/static/src/css/my2_payment_modal.css b/my_compassion/static/src/css/my2_payment_modal.css new file mode 100644 index 000000000..905dfe3fa --- /dev/null +++ b/my_compassion/static/src/css/my2_payment_modal.css @@ -0,0 +1,66 @@ +/* Define the variable for maintainability */ +:root { + /* Represents the total height of modal header/footer or vertical padding to subtract */ + --modal-vertical-padding: 64px; +} + +/* Card Container */ +.payment-method-card { + transition: all 0.2s ease-in-out; + border-width: 1px; +} + +/* Selected State */ +.payment-method-card.selected { + border-width: 10px; +} + +/* Hover State */ +.payment-method-card:not(.selected):hover { + background-color: #f8f9fa; + border-color: #dee2e6; +} + +.payment-method-card .pointer-events-none { + pointer-events: none; +} + +.payment-method-card.hover-shadow-sm:hover { + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); +} + +#payment_method_selector_modal .modal-content { + max-height: 90vh; /* limit modal height */ + overflow: hidden; /* let inner container scroll */ +} + +/* scrollable list that fits inside modal (subtract header/footer space) */ +.payment-methods-list { + /* Using custom property to calculate available height */ + max-height: calc(100vh - var(--modal-vertical-padding)); + overflow-y: auto; + -webkit-overflow-scrolling: touch; +} + +.oe_accordion_arrow { + transition: transform 0.25s ease-in-out; +} + +/* When button has .collapsed class → section is closed → show right arrow */ +.btn.collapsed .oe_accordion_arrow { + transform: rotate(-90deg); /* chevron-down becomes chevron-right */ +} + +/* When expanded → keep down (0deg) */ +.btn:not(.collapsed) .oe_accordion_arrow { + transform: rotate(0deg); +} + +/* Same logic for payment method cards*/ +.cursor-pointer[aria-expanded="false"] .oe_accordion_arrow { + transform: rotate(-90deg); +} + +.cursor-pointer[aria-expanded="true"] .oe_accordion_arrow { + transform: rotate(0deg); +} diff --git a/my_compassion/static/src/js/my2_donations.js b/my_compassion/static/src/js/my2_donations.js index 9c611f247..2dca076e3 100644 --- a/my_compassion/static/src/js/my2_donations.js +++ b/my_compassion/static/src/js/my2_donations.js @@ -1,54 +1,346 @@ -document.addEventListener("DOMContentLoaded", function () { - odoo.define("my_compassion.donations_pager_simple", function (require) { - "use strict"; +odoo.define("my_compassion.my2_donations", function (require) { + "use strict"; - var rpc = require("web.rpc"); - let isUpdating = false; + var publicWidget = require("web.public.widget"); + var core = require("web.core"); + var QWeb = core.qweb; + const ToastService = require("my_compassion.toast_service"); + var _t = core._t; - function updateHistory(page) { - if (isUpdating) { - return; + publicWidget.registry.My2Donations = publicWidget.Widget.extend({ + selector: ".my2-donations-page", + + // Load Client-Side QWeb Templates + xmlDependencies: ["/my_compassion/static/src/xml/my2_payment_method_templates.xml"], + + events: { + // Custom events dispatched from the DOM (e.g. from my2_sponsorships_group_card.xml) + open_payment_method_update: "_onOpenUpdateModal", + open_payment_method_change: "_onOpenChangeModal", + open_payment_method_add: "_onOpenAddModal", + + // UI Interaction events + "click #btn_save_payment_method": "_onSavePaymentMethod", + 'change input[name="payment_method_selection"]': "_onMethodSelectionChange", + "click #history_pager_prev, #history_pager_next": "_onPagerClick", + }, + + /** + * Widget Initialization + */ + start: function () { + var self = this; + + // 1. Initialize Local State + // Read the initial list of payment methods passed from the backend template + var $container = this.$("#my_sponsorships_container"); + this.paymentMethods = $container.data("payment-methods") || []; + + // 2. Global Bindings (Cleanup & Modal behaviors) + this._onModalHiddenGlobalBound = this._onModalHiddenGlobal.bind(this); + this._onMethodSelectionChangeBound = this._onMethodSelectionChange.bind(this); + + $("body").on("hidden.bs.modal", ".modal", this._onModalHiddenGlobalBound); + // We bind change to body to catch inputs inside dynamically rendered modals + $("body").on("change", 'input[name="payment_method_selection"]', this._onMethodSelectionChangeBound); + + // 3. Check for URL flash messages (e.g. after redirects) + this._checkAddPaymentMethod(); + + return this._super.apply(this, arguments); + }, + + destroy: function () { + if (this._onModalHiddenGlobalBound) { + $("body").off("hidden.bs.modal", ".modal", this._onModalHiddenGlobalBound); } + if (this._onMethodSelectionChangeBound) { + $("body").off("change", 'input[name="payment_method_selection"]', this._onMethodSelectionChangeBound); + } + this._super.apply(this, arguments); + }, + + // ------------------------------------------------------------------------- + // MODAL OPEN HANDLERS + // ------------------------------------------------------------------------- + + /** + * Opens the "Change" modal (Moving a contract to a different group/method) + */ + _onOpenChangeModal: function (ev) { + ev.stopPropagation(); + // detail contains: { contract_id, group_id, child_name } passed via detail_js + var detail = ev.detail || {}; + var $container = this.$("#my_sponsorships_container"); + this.paymentMethods = $container.data("payment-methods") || []; + + var $modal = $("#payment_method_selector_modal_change"); + + // Update Description + $modal + .find("#modal_description") + .text( + _.str.sprintf( + _t("Change your payment method for %s."), + detail.child_name || _t("your sponsored child") + ) + ); + + // Store context + $modal.data("contract-id", detail.contract_id); - const historyContainer = document.getElementById("donation_history_container"); - const pagerButtons = document.querySelectorAll("#history_pager_prev, #history_pager_next"); + // Render the list of available methods + // We pass the current group_id to highlight the currently active method + this._renderPaymentMethodsList($modal, detail.group_id, "#modal_container"); - if (!historyContainer) { - console.error("Donation history container not found."); + $modal.modal("show"); + }, + + /** + * Opens the "Update" modal (Editing the current group's details or merging) + */ + _onOpenUpdateModal: function (ev) { + ev.stopPropagation(); + var detail = ev.detail || {}; + // detail contains: { group_id, method_info } + + var $modal = $("#payment_method_selector_modal_update"); + $modal.data("group-id", detail.group_id); + + // 1. Render the "Update Current Details" Form + // We reuse the client-side QWeb template for the form + // Note: detail.method_info comes from the data-attributes we setup in the template + var formHtml = QWeb.render("my_compassion.PaymentMethodUpdateAccordion", detail.method_info || {}); + $modal.find("#modal_container").empty().html(formHtml); + + // 2. Render the "Switch" list in the accordion + this._renderPaymentMethodsList($modal, detail.group_id, "#payment_methods_switch_container"); + + $modal.modal("show"); + }, + + /** + * Opens the "Add" modal + */ + _onOpenAddModal: function (ev) { + ev.stopPropagation(); + $("#payment_method_selector_modal_add").modal("show"); + }, + + // ------------------------------------------------------------------------- + // RENDERING HELPERS + // ------------------------------------------------------------------------- + + /** + * Renders the list of payment method cards into a specific container + * Uses the local `this.paymentMethods` state. + */ + _renderPaymentMethodsList: function ($modal, currentGroupId, containerSelector) { + var $container = $modal.find(containerSelector); + $container.empty(); + + if (!this.paymentMethods || this.paymentMethods.length === 0) { + $container.html(QWeb.render("my_compassion.PaymentMethodLoading")); return; } - isUpdating = true; - pagerButtons.forEach((btn) => btn.classList.add("disabled")); + var self = this; + // Iterate over local state + _.each(this.paymentMethods, function (method) { + // Clone data to avoid mutating state + var data = _.extend({}, method, { + // Mark as selected if it matches the current group of the child + selected: method.group_id == currentGroupId, + }); + + // Render Client-Side Template + var $card = $(QWeb.render("my_compassion.PaymentMethodCard", data)); + $container.append($card); + }); + }, + + // ------------------------------------------------------------------------- + // ACTION HANDLERS + // ------------------------------------------------------------------------- + + _onMethodSelectionChange: function (ev) { + var $input = $(ev.currentTarget); + var $container = $input.closest(".payment-methods-container, #payment_methods_switch_container"); + + // Visual toggle of classes + $container + .find(".payment-method-card") + .removeClass("selected border-core-blue bg-light-blue") + .addClass("border-gray-200 hover-shadow-sm"); + + $input + .closest(".payment-method-card") + .addClass("selected border-core-blue bg-light-blue") + .removeClass("border-gray-200 hover-shadow-sm"); + }, + + _onSavePaymentMethod: function (ev) { + ev.preventDefault(); + var self = this; + var $btn = $(ev.currentTarget); + var $modal = $btn.closest(".modal"); + var modalType = $modal.data("modal-type"); + + // UI Loading State + $btn.prop("disabled", true).prepend(''); + + var promise; + var params = {}; + var route = ""; + + // --- PREPARE PARAMETERS BASED ON MODAL TYPE --- + if (modalType == "change") { + var $selectedInput = $modal.find('input[name="payment_method_selection"]:checked'); + var new_group_id = $selectedInput.attr("group-id"); + + route = "/my2/donation/change_method_contract"; + params = { + contract_id: $modal.data("contract-id"), + group_id: parseInt(new_group_id), + }; + } else if (modalType == "update") { + var currentGroupId = $modal.data("group-id"); + route = "/my2/donation/change_method_group"; + params = { group_id: currentGroupId }; + + // 1. Check for Group Switch + var $selectedGroupInput = $modal.find('input[name="payment_method_selection"]:checked'); + if ($selectedGroupInput.length) { + var selectedGroupId = $selectedGroupInput.attr("group-id"); + if (selectedGroupId && parseInt(selectedGroupId) !== parseInt(currentGroupId)) { + params.new_group_id = parseInt(selectedGroupId); + } + } + + // 2. Check for Detail Updates + var $bvrInput = $modal.find('input[name="ref_number"]'); + if ($bvrInput.length) { + var newBvrRef = $bvrInput.val(); + var oldBvrRef = $bvrInput.prop("defaultValue"); + if (newBvrRef !== oldBvrRef) { + params.new_bvr_ref = newBvrRef; + } + } + + if (!params.new_group_id && !params.new_bvr_ref) { + this._closeModal($modal, $btn); + return; + } + } else if (modalType == "add") { + route = "/my2/donation/add_payment_method_group"; + params = { + method_type: $modal.find('select[name="method_type"]').val(), + recurring_unit: $modal.find('select[name="recurring_unit"]').val(), + advance_billing_months: parseInt($modal.find('input[name="advance_billing_months"]').val()), + }; + } + + // --- EXECUTE RPC REQUEST --- + if (route) { + this._rpc({ + route: route, + params: params, + }) + .then(function (result) { + if (result.success) { + $modal.modal("hide"); + ToastService.success(_t("The operation was successful."), _t("Success")); + + // A. UPDATE HTML (Server-Side Rendered List) + if (result.html) { + var $newContent = $(result.html); + // Replace the specific container + self.$("#my_sponsorships_container").replaceWith($newContent); + } - rpc.query({ + // B. UPDATE STATE (Client-Side Data) + if (result.payment_methods) { + self.paymentMethods = result.payment_methods; + } + } else { + ToastService.error(result.error || _t("An error occurred.")); + } + }) + .finally(function () { + self._closeModal($modal, $btn); + }); + } + }, + + _closeModal: function ($modal, $btn) { + $modal.modal("hide"); + $btn.prop("disabled", false).find(".fa-spinner").remove(); + }, + + _onModalHidden: function ($modal) { + // Cleanup data attached to DOM + $modal.removeData(["group-id", "contract-id"]); + $modal.find('input[type="radio"]').prop("checked", false); + // Also reset form inputs if needed + $modal.find('input[type="text"]').val(function () { + return this.defaultValue; + }); + }, + + _onModalHiddenGlobal: function (ev) { + var $modal = $(ev.target); + if ($modal.attr("id") && $modal.attr("id").startsWith("payment_method_selector_modal")) { + this._onModalHidden($modal); + } + }, + + // ------------------------------------------------------------------------- + // TOASTS & HISTORY + // ------------------------------------------------------------------------- + + _checkAddPaymentMethod: function () { + var urlParams = new URLSearchParams(window.location.search); + var res = urlParams.get("payment_method_result"); + var msg = urlParams.get("payment_method_message"); + + if (res === "Success") ToastService.success(_t(msg), _t(res)); + else if (res === "Error") ToastService.error(_t(msg), _t(res)); + else if (res === "Already Saved") ToastService.info(_t(msg), _t(res)); + + if (res) { + urlParams.delete("payment_method_result"); + urlParams.delete("payment_method_message"); + var newUrl = window.location.pathname + (urlParams.toString() ? "?" + urlParams.toString() : ""); + window.history.replaceState({}, document.title, newUrl); + } + }, + + _onPagerClick: function (ev) { + ev.preventDefault(); + var $btn = $(ev.currentTarget); + if ($btn.hasClass("disabled")) return; + var page = $btn.data("page"); + if (page) this._updateHistory(page); + }, + + _updateHistory: function (page) { + var self = this; + var $container = this.$("#donation_history_container"); + var $buttons = this.$("#history_pager_prev, #history_pager_next"); + $buttons.addClass("disabled"); + + this._rpc({ route: "/my2/donations/history", - params: { - invoice_page: page, - }, + params: { invoice_page: page }, }) .then(function (result) { - if (result.html) { - historyContainer.outerHTML = result.html; + if (result.html && $container.length) { + $container.replaceWith(result.html); } }) - .finally(() => { - isUpdating = false; - document.querySelectorAll("#history_pager_prev, #history_pager_next").forEach((btn) => { - if (btn) btn.classList.remove("disabled"); - }); + .finally(function () { + self.$("#history_pager_prev, #history_pager_next").removeClass("disabled"); }); - } - - document.addEventListener("click", function (event) { - const btn = event.target.closest("#history_pager_prev, #history_pager_next"); - if (btn) { - event.preventDefault(); - const page = btn.dataset.page; - if (page) { - updateHistory(page); - } - } - }); + }, }); }); diff --git a/my_compassion/static/src/xml/my2_payment_method_templates.xml b/my_compassion/static/src/xml/my2_payment_method_templates.xml new file mode 100644 index 000000000..444012bdc --- /dev/null +++ b/my_compassion/static/src/xml/my2_payment_method_templates.xml @@ -0,0 +1,198 @@ + + + + +
+ +
+ +
+ +
+ + Icon + + + + + +
+ +
+
+ +
+ +
+ + + Ref: + + + + + + + Exp: + + + + +
+
+ + + + +
+
+ + +
+ +

Loading payment methods...

+
+
+ + +
+ +
+
+
+ +
+
+
+
+ + +
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+ + + +
+ + + + + + +
+ +
+
+
+
+ + +
+ + + Reference Number + ref_number + + +
+
+ diff --git a/my_compassion/templates/components/my2_donation_item.xml b/my_compassion/templates/components/my2_donation_item.xml index c2d33bb20..6c4cf284e 100644 --- a/my_compassion/templates/components/my2_donation_item.xml +++ b/my_compassion/templates/components/my2_donation_item.xml @@ -23,6 +23,7 @@ - primary_button (dict): Primary button with custom action (see Button). [none] - recipient (string): Item recipient. [none] - subtext (string): Text under name and recipient. [none] + - inline_button (dict): Inline button with custom action (see Button). [none] - title_color (string): Color of the title. [dark-blue] - accent_color (string): Color of the left border accent. [dark-blue] - price_color (string): Color of the price. [dark-blue] @@ -37,13 +38,19 @@