1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-18 07:52:23 +01:00

refactored admin takes subscription for a member

This commit is contained in:
Sylvain 2021-10-19 12:24:41 +02:00
parent fd53c44a83
commit 2d61dac9cc
6 changed files with 141 additions and 145 deletions

View File

@ -3,15 +3,21 @@ import Select from 'react-select';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Subscription } from '../../models/subscription'; import { Subscription } from '../../models/subscription';
import { User } from '../../models/user'; import { User } from '../../models/user';
import { PaymentMethod } from '../../models/payment'; import { PaymentMethod, ShoppingCart } from '../../models/payment';
import { FabModal } from '../base/fab-modal'; import { FabModal } from '../base/fab-modal';
import LocalPaymentAPI from '../../api/local-payment';
import SubscriptionAPI from '../../api/subscription'; import SubscriptionAPI from '../../api/subscription';
import { Plan } from '../../models/plan'; import { Plan } from '../../models/plan';
import PlanAPI from '../../api/plan'; import PlanAPI from '../../api/plan';
import { Loader } from '../base/loader'; import { Loader } from '../base/loader';
import { react2angular } from 'react2angular'; import { react2angular } from 'react2angular';
import { IApplication } from '../../models/application'; import { IApplication } from '../../models/application';
import FormatLib from '../../lib/format';
import { SelectSchedule } from '../payment-schedule/select-schedule';
import { ComputePriceResult } from '../../models/price';
import { PaymentScheduleSummary } from '../payment-schedule/payment-schedule-summary';
import { PaymentSchedule } from '../../models/payment-schedule';
import PriceAPI from '../../api/price';
import { LocalPaymentModal } from '../payment/local-payment/local-payment-modal';
declare const Application: IApplication; declare const Application: IApplication;
@ -19,6 +25,7 @@ interface SubscribeModalProps {
isOpen: boolean, isOpen: boolean,
toggleModal: () => void, toggleModal: () => void,
customer: User, customer: User,
operator: User,
onSuccess: (message: string, subscription: Subscription) => void, onSuccess: (message: string, subscription: Subscription) => void,
onError: (message: string) => void, onError: (message: string) => void,
} }
@ -30,56 +37,90 @@ interface SubscribeModalProps {
type selectOption = { value: number, label: string }; type selectOption = { value: number, label: string };
/** /**
* Modal dialog shown to create a subscription for teh given customer * Modal dialog shown to create a subscription for the given customer
*/ */
const SubscribeModal: React.FC<SubscribeModalProps> = ({ isOpen, toggleModal, customer, onError, onSuccess }) => { const SubscribeModal: React.FC<SubscribeModalProps> = ({ isOpen, toggleModal, customer, operator, onError, onSuccess }) => {
const { t } = useTranslation('admin'); const { t } = useTranslation('admin');
const [plan, setPlan] = useState<number>(null); const [selectedPlan, setSelectedPlan] = useState<Plan>(null);
const [plans, setPlans] = useState<Array<Plan>>(null); const [selectedSchedule, setSelectedSchedule] = useState<boolean>(false);
const [allPlans, setAllPlans] = useState<Array<Plan>>(null);
const [price, setPrice] = useState<ComputePriceResult>(null);
const [cart, setCart] = useState<ShoppingCart>(null);
const [localPaymentModal, setLocalPaymentModal] = useState<boolean>(false);
// fetch all plans from the API on component mount // fetch all plans from the API on component mount
useEffect(() => { useEffect(() => {
PlanAPI.index() PlanAPI.index()
.then(allPlans => setPlans(allPlans)) .then(plans => setAllPlans(plans))
.catch(error => onError(error)); .catch(error => onError(error));
}, []); }, []);
/** // when the plan is updated, update the default value for the payment schedule requirement
* Callback triggered when the user validates the subscription useEffect(() => {
*/ if (!selectedPlan) return;
const handleConfirmSubscribe = (): void => {
LocalPaymentAPI.confirmPayment({ setSelectedSchedule(selectedPlan.monthly_payment);
}, [selectedPlan]);
// when the plan or the requirement for a payment schedule are updated, update the cart accordingly
useEffect(() => {
if (!selectedPlan) return;
setCart({
customer_id: customer.id, customer_id: customer.id,
payment_method: PaymentMethod.Other, items: [{
items: [
{
subscription: { subscription: {
plan_id: plan plan_id: selectedPlan.id
} }
} }],
] payment_method: PaymentMethod.Other,
}).then(res => { payment_schedule: selectedSchedule
SubscriptionAPI.get(res.main_object.id).then(subscription => { });
onSuccess(t('app.admin.subscribe_modal.subscription_success'), subscription); }, [selectedSchedule, selectedPlan]);
toggleModal();
}).catch(error => onError(error)); // when the cart is updated, update the price accordingly
}).catch(err => onError(err)); useEffect(() => {
}; if (!cart) return;
PriceAPI.compute(cart)
.then(res => setPrice(res))
.catch(err => onError(err));
}, [cart]);
/** /**
* Callback triggered when the user selects a group in the dropdown list * Callback triggered when the user selects a group in the dropdown list
*/ */
const handlePlanSelect = (option: selectOption): void => { const handlePlanSelect = (option: selectOption): void => {
setPlan(option.value); const plan = allPlans.find(p => p.id === option.value);
setSelectedPlan(plan);
};
/**
* Callback triggered when the payment of the subscription was successful
*/
const onPaymentSuccess = (res): void => {
SubscriptionAPI.get(res.main_object.id).then(subscription => {
onSuccess(t('app.admin.subscribe_modal.subscription_success'), subscription);
toggleModal();
}).catch(error => onError(error));
};
/**
* Open/closes the local payment modal
*/
const toggleLocalPaymentModal = (): void => {
setLocalPaymentModal(!localPaymentModal);
}; };
/** /**
* Convert all groups to the react-select format * Convert all groups to the react-select format
*/ */
const buildOptions = (): Array<selectOption> => { const buildOptions = (): Array<selectOption> => {
return plans.filter(p => !p.disabled).map(p => { if (!allPlans) return [];
return { value: p.id, label: p.base_name };
return allPlans.filter(p => !p.disabled && p.group_id === customer.group_id).map(p => {
return { value: p.id, label: `${p.base_name} (${FormatLib.duration(p.interval, p.interval_count)})` };
}); });
}; };
@ -89,22 +130,42 @@ const SubscribeModal: React.FC<SubscribeModalProps> = ({ isOpen, toggleModal, cu
className="subscribe-modal" className="subscribe-modal"
title={t('app.admin.subscribe_modal.subscribe_USER', { USER: customer.name })} title={t('app.admin.subscribe_modal.subscribe_USER', { USER: customer.name })}
confirmButton={t('app.admin.subscribe_modal.subscribe')} confirmButton={t('app.admin.subscribe_modal.subscribe')}
onConfirm={handleConfirmSubscribe} onConfirm={toggleLocalPaymentModal}
closeButton> closeButton>
<div className="options">
<label htmlFor="select-plan">{t('app.admin.subscribe_modal.select_plan')}</label> <label htmlFor="select-plan">{t('app.admin.subscribe_modal.select_plan')}</label>
<Select id="select-plan" <Select id="select-plan"
onChange={handlePlanSelect} onChange={handlePlanSelect}
options={buildOptions()} /> options={buildOptions()} />
<SelectSchedule show={selectedPlan?.monthly_payment} selected={selectedSchedule} onChange={setSelectedSchedule} />
</div>
<div className="summary">
{price?.schedule && <PaymentScheduleSummary schedule={price.schedule as PaymentSchedule} />}
{price && !price.schedule && <div className="one-go-payment">
<h4>{t('app.admin.subscribe_modal.pay_in_one_go')}</h4>
<span>{FormatLib.price(price.price)}</span>
</div>}
</div>
<LocalPaymentModal isOpen={localPaymentModal}
toggleModal={toggleLocalPaymentModal}
afterSuccess={onPaymentSuccess}
onError={onError}
cart={cart}
updateCart={setCart}
currentUser={operator}
customer={customer}
schedule={price?.schedule as PaymentSchedule} />
</FabModal> </FabModal>
); );
}; };
const SubscribeModalWrapper: React.FC<SubscribeModalProps> = ({ isOpen, toggleModal, customer, onError, onSuccess }) => { const SubscribeModalWrapper: React.FC<SubscribeModalProps> = ({ isOpen, toggleModal, customer, operator, onError, onSuccess }) => {
return ( return (
<Loader> <Loader>
<SubscribeModal isOpen={isOpen} toggleModal={toggleModal} customer={customer} onSuccess={onSuccess} onError={onError} /> <SubscribeModal isOpen={isOpen} toggleModal={toggleModal} customer={customer} operator={operator} onSuccess={onSuccess} onError={onError} />
</Loader> </Loader>
); );
}; };
Application.Components.component('subscribeModal', react2angular(SubscribeModalWrapper, ['toggleModal', 'isOpen', 'customer', 'onError', 'onSuccess'])); Application.Components.component('subscribeModal', react2angular(SubscribeModalWrapper, ['toggleModal', 'isOpen', 'customer', 'operator', 'onError', 'onSuccess']));

View File

@ -711,6 +711,9 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
// modal dialog to renew the current subscription // modal dialog to renew the current subscription
$scope.isOpenRenewModal = false; $scope.isOpenRenewModal = false;
// modal dialog to take a new subscription
$scope.isOpenSubscribeModal = false;
/** /**
* Open a modal dialog asking for confirmation to change the role of the given user * Open a modal dialog asking for confirmation to change the role of the given user
* @returns {*} * @returns {*}
@ -778,6 +781,15 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
}, 50); }, 50);
}; };
/**
* Opens/closes the modal dialog to renew the subscription (with payment)
*/
$scope.toggleSubscribeModal = () => {
setTimeout(() => {
$scope.isOpenSubscribeModal = !$scope.isOpenSubscribeModal;
$scope.$apply();
}, 50);
};
/** /**
* Callback triggered if the subscription was successfully extended * Callback triggered if the subscription was successfully extended
*/ */
@ -786,6 +798,14 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
$scope.subscription.expired_at = newExpirationDate; $scope.subscription.expired_at = newExpirationDate;
}; };
/**
* Callback triggered if a new subscription was successfully taken
*/
$scope.onSubscribeSuccess = (message, newSubscription) => {
growl.success(message);
$scope.subscription = newSubscription;
};
/** /**
* Callback triggered in case of error * Callback triggered in case of error
*/ */
@ -793,88 +813,6 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
growl.error(message); growl.error(message);
}; };
/**
* Open a modal dialog allowing the admin to set a subscription for the given user.
* @param user {Object} User object, user currently reviewed, as recovered from GET /api/members/:id
* @param plans {Array} List of plans, available for the currently reviewed user, as recovered from GET /api/plans
*/
$scope.createSubscriptionModal = function (user, plans) {
const modalInstance = $uibModal.open({
animation: true,
templateUrl: '/admin/subscriptions/create_modal.html',
size: 'lg',
controller: ['$scope', '$uibModalInstance', 'Subscription', function ($scope, $uibModalInstance, Subscription) {
// selected user
$scope.user = user;
// available plans for the selected user
$scope.plans = plans;
// default parameters for the new subscription
$scope.subscription = {
payment_schedule: false,
payment_method: 'check'
};
/**
* Generate a string identifying the given plan by literal human-readable name
* @param plan {Object} Plan object, as recovered from GET /api/plan/:id
* @param groups {Array} List of Groups objects, as recovered from GET /api/groups
* @param short {boolean} If true, the generated name will contain the group slug, otherwise the group full name
* will be included.
* @returns {String}
*/
$scope.humanReadablePlanName = function (plan, groups, short) { return `${$filter('humanReadablePlanName')(plan, groups, short)}`; };
/**
* Check if the currently selected plan can be paid with a payment schedule or not
* @return {boolean}
*/
$scope.allowMonthlySchedule = function () {
if (!$scope.subscription) return false;
const plan = plans.find(p => p.id === $scope.subscription.plan_id);
return plan && plan.monthly_payment;
};
/**
* Triggered by the <switch> component.
* We must use a setTimeout to workaround the react integration.
* @param checked {Boolean}
*/
$scope.toggleSchedule = function (checked) {
setTimeout(() => {
$scope.subscription.payment_schedule = checked;
$scope.$apply();
}, 50);
};
/**
* Modal dialog validation callback
*/
$scope.ok = function () {
$scope.subscription.user_id = user.id;
return Subscription.save({ }, { subscription: $scope.subscription }, function (_subscription) {
growl.success(_t('app.admin.members_edit.subscription_successfully_purchased'));
$uibModalInstance.close(_subscription);
return $state.reload();
}
, function (error) {
growl.error(_t('app.admin.members_edit.a_problem_occurred_while_taking_the_subscription'));
console.error(error);
});
};
/**
* Modal dialog cancellation callback
*/
$scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
}]
});
// once the form was validated successfully ...
return modalInstance.result.then(function (subscription) { $scope.subscription = subscription; });
};
$scope.createWalletCreditModal = function (user, wallet) { $scope.createWalletCreditModal = function (user, wallet) {
const modalInstance = $uibModal.open({ const modalInstance = $uibModal.open({
animation: true, animation: true,

View File

@ -1,4 +1,4 @@
import moment from 'moment'; import moment, { unitOfTime } from 'moment';
import { IFablab } from '../models/fablab'; import { IFablab } from '../models/fablab';
declare let Fablab: IFablab; declare let Fablab: IFablab;
@ -18,6 +18,13 @@ export default class FormatLib {
return Intl.DateTimeFormat(Fablab.intl_locale, { hour: 'numeric', minute: 'numeric' }).format(moment(date).toDate()); return Intl.DateTimeFormat(Fablab.intl_locale, { hour: 'numeric', minute: 'numeric' }).format(moment(date).toDate());
}; };
/**
* Return the formatted localized duration
*/
static duration = (interval: unitOfTime.DurationConstructor, intervalCount: number): string => {
return moment.duration(intervalCount, interval).locale(Fablab.moment_locale).humanize();
}
/** /**
* Return the formatted localized amount for the given price (eg. 20.5 => "20,50 €") * Return the formatted localized amount for the given price (eg. 20.5 => "20,50 €")
*/ */

View File

@ -105,7 +105,14 @@
<p translate> <p translate>
{{ 'app.admin.members_edit.user_has_no_current_subscription' }} {{ 'app.admin.members_edit.user_has_no_current_subscription' }}
</p> </p>
<button class="btn btn-default" ng-click="createSubscriptionModal(user, plans.filter(filterDisabledPlans))" translate>{{ 'app.admin.members_edit.subscribe_to_a_plan' }}</button> <button class="btn btn-default" ng-click="toggleSubscribeModal()" translate>{{ 'app.admin.members_edit.subscribe_to_a_plan' }}</button>
<subscribe-modal is-open="isOpenSubscribeModal"
toggle-modal="toggleSubscribeModal"
customer="user"
operator="currentUser"
on-error="onError"
on-success="onSubscribeSuccess">
</subscribe-modal>
</div> </div>
</div> </div>

View File

@ -1,24 +0,0 @@
<div class="modal-header">
<h3 class="modal-title m-l" translate>{{ 'app.admin.members_edit.new_subscription' }}</h3>
</div>
<div class="modal-body m-lg">
<div class="alert alert-danger">
<p translate translate-values="{NAME: user.name}">
{{ 'app.admin.members_edit.you_are_about_to_purchase_a_subscription_to_NAME' }}
</p>
</div>
<form role="form" name="subscriptionForm" class="form-horizontal" novalidate>
<div class="form-group">
<select ng-model="subscription.plan_id" ng-options="plan.id as humanReadablePlanName(plan) for plan in plans" class="form-control" required>
</select>
</div>
<div class="form-group" ng-show="allowMonthlySchedule()">
<label for="schedule" class="control-label m-r-md">{{ 'app.admin.members_edit.with_schedule' | translate }}</label>
<switch id="schedule" checked="subscription.payment_schedule" on-change="toggleSchedule"></switch>
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-warning" ng-click="ok()" ng-disabled="subscriptionForm.$invalid" translate>{{ 'app.shared.buttons.confirm' }}</button>
<button class="btn btn-primary" ng-click="cancel()" translate>{{ 'app.shared.buttons.cancel' }}</button>
</div>

View File

@ -925,6 +925,13 @@ en:
pay_in_one_go: "Pay in one go" pay_in_one_go: "Pay in one go"
renew: "Renew" renew: "Renew"
renew_success: "The subscription was successfully renewed" renew_success: "The subscription was successfully renewed"
# take a new subscription
subscribe_modal:
subscribe_USER: "Subscribe for {USER}"
subscribe: "Subscribe"
select_plan: "Please select a plan"
pay_in_one_go: "Pay in one go"
subscription_success: ""
#add a new administrator to the platform #add a new administrator to the platform
admins_new: admins_new:
add_an_administrator: "Add an administrator" add_an_administrator: "Add an administrator"