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:
parent
fd53c44a83
commit
2d61dac9cc
@ -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: {
|
||||||
{
|
plan_id: selectedPlan.id
|
||||||
subscription: {
|
|
||||||
plan_id: plan
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
}],
|
||||||
}).then(res => {
|
payment_method: PaymentMethod.Other,
|
||||||
SubscriptionAPI.get(res.main_object.id).then(subscription => {
|
payment_schedule: selectedSchedule
|
||||||
onSuccess(t('app.admin.subscribe_modal.subscription_success'), subscription);
|
});
|
||||||
toggleModal();
|
}, [selectedSchedule, selectedPlan]);
|
||||||
}).catch(error => onError(error));
|
|
||||||
}).catch(err => onError(err));
|
// 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
|
* 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>
|
||||||
<label htmlFor="select-plan">{t('app.admin.subscribe_modal.select_plan')}</label>
|
<div className="options">
|
||||||
<Select id="select-plan"
|
<label htmlFor="select-plan">{t('app.admin.subscribe_modal.select_plan')}</label>
|
||||||
onChange={handlePlanSelect}
|
<Select id="select-plan"
|
||||||
options={buildOptions()} />
|
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>
|
</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']));
|
||||||
|
@ -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,
|
||||||
|
@ -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 €")
|
||||||
*/
|
*/
|
||||||
|
@ -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>
|
||||||
|
@ -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"
|
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"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user