mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-01-17 06:52:27 +01:00
refactored admin takes subscription for a member
This commit is contained in:
parent
fd53c44a83
commit
2d61dac9cc
@ -3,15 +3,21 @@ import Select from 'react-select';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Subscription } from '../../models/subscription';
|
||||
import { User } from '../../models/user';
|
||||
import { PaymentMethod } from '../../models/payment';
|
||||
import { PaymentMethod, ShoppingCart } from '../../models/payment';
|
||||
import { FabModal } from '../base/fab-modal';
|
||||
import LocalPaymentAPI from '../../api/local-payment';
|
||||
import SubscriptionAPI from '../../api/subscription';
|
||||
import { Plan } from '../../models/plan';
|
||||
import PlanAPI from '../../api/plan';
|
||||
import { Loader } from '../base/loader';
|
||||
import { react2angular } from 'react2angular';
|
||||
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;
|
||||
|
||||
@ -19,6 +25,7 @@ interface SubscribeModalProps {
|
||||
isOpen: boolean,
|
||||
toggleModal: () => void,
|
||||
customer: User,
|
||||
operator: User,
|
||||
onSuccess: (message: string, subscription: Subscription) => void,
|
||||
onError: (message: string) => void,
|
||||
}
|
||||
@ -30,56 +37,90 @@ interface SubscribeModalProps {
|
||||
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 [plan, setPlan] = useState<number>(null);
|
||||
const [plans, setPlans] = useState<Array<Plan>>(null);
|
||||
const [selectedPlan, setSelectedPlan] = useState<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
|
||||
useEffect(() => {
|
||||
PlanAPI.index()
|
||||
.then(allPlans => setPlans(allPlans))
|
||||
.then(plans => setAllPlans(plans))
|
||||
.catch(error => onError(error));
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Callback triggered when the user validates the subscription
|
||||
*/
|
||||
const handleConfirmSubscribe = (): void => {
|
||||
LocalPaymentAPI.confirmPayment({
|
||||
// when the plan is updated, update the default value for the payment schedule requirement
|
||||
useEffect(() => {
|
||||
if (!selectedPlan) return;
|
||||
|
||||
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,
|
||||
payment_method: PaymentMethod.Other,
|
||||
items: [
|
||||
{
|
||||
subscription: {
|
||||
plan_id: plan
|
||||
}
|
||||
items: [{
|
||||
subscription: {
|
||||
plan_id: selectedPlan.id
|
||||
}
|
||||
]
|
||||
}).then(res => {
|
||||
SubscriptionAPI.get(res.main_object.id).then(subscription => {
|
||||
onSuccess(t('app.admin.subscribe_modal.subscription_success'), subscription);
|
||||
toggleModal();
|
||||
}).catch(error => onError(error));
|
||||
}).catch(err => onError(err));
|
||||
};
|
||||
}],
|
||||
payment_method: PaymentMethod.Other,
|
||||
payment_schedule: selectedSchedule
|
||||
});
|
||||
}, [selectedSchedule, selectedPlan]);
|
||||
|
||||
// when the cart is updated, update the price accordingly
|
||||
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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
const buildOptions = (): Array<selectOption> => {
|
||||
return plans.filter(p => !p.disabled).map(p => {
|
||||
return { value: p.id, label: p.base_name };
|
||||
if (!allPlans) return [];
|
||||
|
||||
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"
|
||||
title={t('app.admin.subscribe_modal.subscribe_USER', { USER: customer.name })}
|
||||
confirmButton={t('app.admin.subscribe_modal.subscribe')}
|
||||
onConfirm={handleConfirmSubscribe}
|
||||
onConfirm={toggleLocalPaymentModal}
|
||||
closeButton>
|
||||
<label htmlFor="select-plan">{t('app.admin.subscribe_modal.select_plan')}</label>
|
||||
<Select id="select-plan"
|
||||
onChange={handlePlanSelect}
|
||||
options={buildOptions()} />
|
||||
<div className="options">
|
||||
<label htmlFor="select-plan">{t('app.admin.subscribe_modal.select_plan')}</label>
|
||||
<Select id="select-plan"
|
||||
onChange={handlePlanSelect}
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
const SubscribeModalWrapper: React.FC<SubscribeModalProps> = ({ isOpen, toggleModal, customer, onError, onSuccess }) => {
|
||||
const SubscribeModalWrapper: React.FC<SubscribeModalProps> = ({ isOpen, toggleModal, customer, operator, onError, onSuccess }) => {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
Application.Components.component('subscribeModal', react2angular(SubscribeModalWrapper, ['toggleModal', 'isOpen', 'customer', 'onError', 'onSuccess']));
|
||||
Application.Components.component('subscribeModal', react2angular(SubscribeModalWrapper, ['toggleModal', 'isOpen', 'customer', 'operator', 'onError', 'onSuccess']));
|
||||
|
@ -711,6 +711,9 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
|
||||
// modal dialog to renew the current subscription
|
||||
$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
|
||||
* @returns {*}
|
||||
@ -778,6 +781,15 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
|
||||
}, 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
|
||||
*/
|
||||
@ -786,6 +798,14 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
|
||||
$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
|
||||
*/
|
||||
@ -793,88 +813,6 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
|
||||
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) {
|
||||
const modalInstance = $uibModal.open({
|
||||
animation: true,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import moment from 'moment';
|
||||
import moment, { unitOfTime } from 'moment';
|
||||
import { IFablab } from '../models/fablab';
|
||||
|
||||
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 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 €")
|
||||
*/
|
||||
|
@ -105,7 +105,14 @@
|
||||
<p translate>
|
||||
{{ 'app.admin.members_edit.user_has_no_current_subscription' }}
|
||||
</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>
|
||||
|
@ -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>
|
@ -925,6 +925,13 @@ en:
|
||||
pay_in_one_go: "Pay in one go"
|
||||
renew: "Renew"
|
||||
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
|
||||
admins_new:
|
||||
add_an_administrator: "Add an administrator"
|
||||
|
Loading…
x
Reference in New Issue
Block a user