mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2024-12-01 12:24:28 +01:00
move free extend modal to react
This commit is contained in:
parent
e646eb8cb5
commit
70f0e21543
20
app/frontend/src/javascript/api/subscription.ts
Normal file
20
app/frontend/src/javascript/api/subscription.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import apiClient from './clients/api-client';
|
||||
import { Subscription, SubscriptionPaymentDetails, UpdateSubscriptionRequest } from '../models/subscription';
|
||||
import { AxiosResponse } from 'axios';
|
||||
|
||||
export default class SubscriptionAPI {
|
||||
static async update (request: UpdateSubscriptionRequest): Promise<Subscription> {
|
||||
const res: AxiosResponse<Subscription> = await apiClient.patch(`/api/subscriptions/${request.id}`, { subscription: request });
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async get (id: number): Promise<Subscription> {
|
||||
const res: AxiosResponse<Subscription> = await apiClient.get(`/api/subscriptions/${id}`);
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async paymentsDetails (id: number): Promise<SubscriptionPaymentDetails> {
|
||||
const res: AxiosResponse<SubscriptionPaymentDetails> = await apiClient.get(`/api/subscriptions/${id}/payment_details`);
|
||||
return res?.data;
|
||||
}
|
||||
}
|
@ -1,11 +1,8 @@
|
||||
import React from 'react';
|
||||
import moment from 'moment';
|
||||
import { FabModal } from '../base/fab-modal';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { HtmlTranslate } from '../base/html-translate';
|
||||
import { IFablab } from '../../models/fablab';
|
||||
|
||||
declare let Fablab: IFablab;
|
||||
import FormatLib from '../../lib/format';
|
||||
|
||||
interface PendingTrainingModalProps {
|
||||
isOpen: boolean,
|
||||
@ -24,9 +21,7 @@ export const PendingTrainingModal: React.FC<PendingTrainingModalProps> = ({ isOp
|
||||
* Return the formatted localized date for the given date
|
||||
*/
|
||||
const formatDateTime = (date: Date): string => {
|
||||
const day = Intl.DateTimeFormat().format(moment(date).toDate());
|
||||
const time = Intl.DateTimeFormat(Fablab.intl_locale, { hour: 'numeric', minute: 'numeric' }).format(moment(date).toDate());
|
||||
return t('app.logged.pending_training_modal.DATE_TIME', { DATE: day, TIME: time });
|
||||
return t('app.logged.pending_training_modal.DATE_TIME', { DATE: FormatLib.date(date), TIME: FormatLib.time(date) });
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -77,6 +77,7 @@ const PaymentScheduleSummary: React.FC<PaymentScheduleSummaryProps> = ({ schedul
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PaymentScheduleSummaryWrapper: React.FC<PaymentScheduleSummaryProps> = ({ schedule }) => {
|
||||
return (
|
||||
<Loader>
|
||||
|
@ -2,13 +2,11 @@ import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import moment from 'moment';
|
||||
import _ from 'lodash';
|
||||
import { IFablab } from '../../models/fablab';
|
||||
import { Plan } from '../../models/plan';
|
||||
import { User, UserRole } from '../../models/user';
|
||||
import { Loader } from '../base/loader';
|
||||
import '../../lib/i18n';
|
||||
|
||||
declare let Fablab: IFablab;
|
||||
import FormatLib from '../../lib/format';
|
||||
|
||||
interface PlanCardProps {
|
||||
plan: Plan,
|
||||
@ -29,14 +27,14 @@ const PlanCardComponent: React.FC<PlanCardProps> = ({ plan, userId, subscribedPl
|
||||
* Return the formatted localized amount of the given plan (eg. 20.5 => "20,50 €")
|
||||
*/
|
||||
const amount = () : string => {
|
||||
return new Intl.NumberFormat(Fablab.intl_locale, { style: 'currency', currency: Fablab.intl_currency }).format(plan.amount);
|
||||
return FormatLib.price(plan.amount);
|
||||
};
|
||||
/**
|
||||
* Return the formatted localized amount, divided by the number of months (eg. 120 => "10,00 € / month")
|
||||
*/
|
||||
const monthlyAmount = (): string => {
|
||||
const monthly = plan.amount / moment.duration(plan.interval_count, plan.interval).asMonths();
|
||||
return new Intl.NumberFormat(Fablab.intl_locale, { style: 'currency', currency: Fablab.intl_currency }).format(monthly);
|
||||
return FormatLib.price(monthly);
|
||||
};
|
||||
/**
|
||||
* Return the formatted localized duration of te given plan (eg. Month/3 => "3 mois")
|
||||
|
@ -17,6 +17,7 @@ import { Price } from '../../models/price';
|
||||
import PrepaidPackAPI from '../../api/prepaid-pack';
|
||||
import { PrepaidPack } from '../../models/prepaid-pack';
|
||||
import { useImmer } from 'use-immer';
|
||||
import FormatLib from '../../lib/format';
|
||||
|
||||
declare let Fablab: IFablab;
|
||||
declare const Application: IApplication;
|
||||
@ -63,11 +64,11 @@ const MachinesPricing: React.FC<MachinesPricingProps> = ({ onError, onSuccess })
|
||||
const hourlyRate = 10;
|
||||
|
||||
if (type === 'hourly_rate') {
|
||||
return new Intl.NumberFormat(Fablab.intl_locale, { style: 'currency', currency: Fablab.intl_currency }).format(hourlyRate);
|
||||
return FormatLib.price(hourlyRate);
|
||||
}
|
||||
|
||||
const price = (hourlyRate / 60) * EXEMPLE_DURATION;
|
||||
return new Intl.NumberFormat(Fablab.intl_locale, { style: 'currency', currency: Fablab.intl_currency }).format(price);
|
||||
return FormatLib.price(price);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -111,7 +112,7 @@ const MachinesPricing: React.FC<MachinesPricingProps> = ({ onError, onSuccess })
|
||||
<div className="machines-pricing">
|
||||
<FabAlert level="warning">
|
||||
<p><HtmlTranslate trKey="app.admin.machines_pricing.prices_match_machine_hours_rates_html"/></p>
|
||||
<p><HtmlTranslate trKey="app.admin.machines_pricing.prices_calculated_on_hourly_rate_html" options={{ DURATION: EXEMPLE_DURATION, RATE: examplePrice('hourly_rate'), PRICE: examplePrice('final_price') }} /></p>
|
||||
<p><HtmlTranslate trKey="app.admin.machines_pricing.prices_calculated_on_hourly_rate_html" options={{ DURATION: `${EXEMPLE_DURATION}`, RATE: examplePrice('hourly_rate'), PRICE: examplePrice('final_price') }} /></p>
|
||||
<p>{t('app.admin.machines_pricing.you_can_override')}</p>
|
||||
</FabAlert>
|
||||
<table>
|
||||
|
@ -0,0 +1,112 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Subscription } from '../../models/subscription';
|
||||
import { FabModal, ModalSize } from '../base/fab-modal';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FabAlert } from '../base/fab-alert';
|
||||
import { FabInput } from '../base/fab-input';
|
||||
import FormatLib from '../../lib/format';
|
||||
import { Loader } from '../base/loader';
|
||||
import { react2angular } from 'react2angular';
|
||||
import { IApplication } from '../../models/application';
|
||||
import SubscriptionAPI from '../../api/subscription';
|
||||
|
||||
declare const Application: IApplication;
|
||||
|
||||
interface FreeExtendModalProps {
|
||||
isOpen: boolean,
|
||||
toggleModal: () => void,
|
||||
subscription: Subscription,
|
||||
onSuccess: (subscription: Subscription) => void,
|
||||
onError: (message: string) => void,
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal dialog shown to extend the current subscription of a customer, for free
|
||||
*/
|
||||
const FreeExtendModal: React.FC<FreeExtendModalProps> = ({ isOpen, toggleModal, subscription, onError, onSuccess }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
const [expirationDate, setExpirationDate] = useState<Date>(new Date(subscription.expired_at));
|
||||
const [freeDays, setFreeDays] = useState<number>(0);
|
||||
|
||||
// we update the number of free days when the new expiration date is updated
|
||||
useEffect(() => {
|
||||
if (!expirationDate || !subscription.expired_at) {
|
||||
setFreeDays(0);
|
||||
}
|
||||
// 86400000 = 1000 * 3600 * 24 = number of ms per day
|
||||
setFreeDays(Math.ceil((expirationDate.getTime() - new Date(subscription.expired_at).getTime()) / 86400000));
|
||||
}, [expirationDate]);
|
||||
|
||||
/**
|
||||
* Return the formatted localized date for the given date
|
||||
*/
|
||||
const formatDateTime = (date: Date): string => {
|
||||
return t('app.admin.free_extend_modal.DATE_TIME', { DATE: FormatLib.date(date), TIME: FormatLib.time(date) });
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the given date formatted for the HTML input-date
|
||||
*/
|
||||
const formatDefaultDate = (date: Date): string => {
|
||||
return date.toISOString().substr(0, 10);
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse the given date and record it as the new expiration date of the subscription
|
||||
*/
|
||||
const handleDateUpdate = (date: string): void => {
|
||||
setExpirationDate(new Date(Date.parse(date)));
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when the user validates the free extent of the subscription
|
||||
*/
|
||||
const handleConfirmExtend = (): void => {
|
||||
SubscriptionAPI.update({
|
||||
id: subscription.id,
|
||||
expired_at: expirationDate,
|
||||
free: true
|
||||
}).then(res => onSuccess(res))
|
||||
.catch(err => onError(err));
|
||||
};
|
||||
|
||||
return (
|
||||
<FabModal isOpen={isOpen}
|
||||
toggleModal={toggleModal}
|
||||
width={ModalSize.large}
|
||||
className="free-extend-modal"
|
||||
title={t('app.admin.free_extend_modal.extend_subscription')}
|
||||
confirmButton={t('app.admin.free_extend_modal.extend')}
|
||||
onConfirm={handleConfirmExtend}
|
||||
closeButton>
|
||||
<FabAlert level="danger" className="conditions">
|
||||
<p>{t('app.admin.free_extend_modal.offer_free_days_infos')}</p>
|
||||
<p>{t('app.admin.free_extend_modal.credits_will_remain_unchanged')}</p>
|
||||
</FabAlert>
|
||||
<form className="configuration-form">
|
||||
<label htmlFor="current_expiration">{t('app.admin.free_extend_modal.current_expiration')}</label>
|
||||
<FabInput id="current_expiration"
|
||||
defaultValue={formatDateTime(subscription.expired_at)}
|
||||
readOnly />
|
||||
<label htmlFor="new_expiration">{t('app.admin.free_extend_modal.new_expiration_date')}</label>
|
||||
<FabInput id="new_expiration"
|
||||
type="date"
|
||||
defaultValue={formatDefaultDate(expirationDate)}
|
||||
onChange={handleDateUpdate} />
|
||||
<label htmlFor="free_days">{t('app.admin.free_extend_modal.number_of_free_days')}</label>
|
||||
<input id="free_days" className="free-days" value={freeDays} readOnly />
|
||||
</form>
|
||||
</FabModal>
|
||||
);
|
||||
};
|
||||
|
||||
const FreeExtendModalWrapper: React.FC<FreeExtendModalProps> = ({ toggleModal, subscription, isOpen, onSuccess, onError }) => {
|
||||
return (
|
||||
<Loader>
|
||||
<FreeExtendModal toggleModal={toggleModal} subscription={subscription} isOpen={isOpen} onError={onError} onSuccess={onSuccess} />
|
||||
</Loader>
|
||||
);
|
||||
};
|
||||
|
||||
Application.Components.component('freeExtendModal', react2angular(FreeExtendModalWrapper, ['toggleModal', 'subscription', 'isOpen', 'onError', 'onSuccess']));
|
@ -705,6 +705,9 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
|
||||
// current active authentication provider
|
||||
$scope.activeProvider = activeProviderPromise;
|
||||
|
||||
// modal dialog to extend the current subscription for free
|
||||
$scope.isOpenFreeExtendModal = false;
|
||||
|
||||
/**
|
||||
* Open a modal dialog asking for confirmation to change the role of the given user
|
||||
* @returns {*}
|
||||
@ -752,6 +755,27 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Opens/closes the modal dialog to freely extend the subscription
|
||||
*/
|
||||
$scope.toggleFreeExtendModal = () => {
|
||||
$scope.isOpenFreeExtendModal = !$scope.isOpenFreeExtendModal;
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered if the subscription was successfully extended
|
||||
*/
|
||||
$scope.onExtendSuccess = (subscription) => {
|
||||
$scope.subscription = subscription;
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered in case of error
|
||||
*/
|
||||
$scope.onError = (message) => {
|
||||
growl.error(message);
|
||||
};
|
||||
|
||||
/**
|
||||
* Open a modal dialog, allowing the admin to extend the current user's subscription (freely or not)
|
||||
* @param subscription {Object} User's subscription object
|
||||
@ -772,7 +796,6 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
|
||||
|
||||
$scope.expire_at = subscription.expired_at;
|
||||
$scope.new_expired_at = new Date(subscription.expired_at);
|
||||
$scope.free = free;
|
||||
$scope.days = 0;
|
||||
$scope.payment_details = paymentDetails;
|
||||
$scope.datePicker = {
|
||||
@ -790,11 +813,6 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
|
||||
return $scope.datePicker.opened = true;
|
||||
};
|
||||
|
||||
$scope.$watch(scope => scope.expire_at
|
||||
, () => refreshDays());
|
||||
$scope.$watch(scope => scope.new_expired_at
|
||||
, () => refreshDays());
|
||||
|
||||
$scope.ok = function () {
|
||||
Subscription.update(
|
||||
{ id: subscription.id },
|
||||
@ -823,17 +841,6 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the number of free days depending on the original subscription expiration date and on the new selected date
|
||||
*/
|
||||
function refreshDays () {
|
||||
if (!$scope.new_expired_at || !$scope.expire_at) {
|
||||
return $scope.days = 0;
|
||||
}
|
||||
// 86400000 = 1000 * 3600 * 24 = number of ms per day
|
||||
$scope.days = Math.round((new Date($scope.new_expired_at).getTime() - new Date($scope.expire_at).getTime()) / 86400000);
|
||||
}
|
||||
|
||||
// !!! MUST BE CALLED AT THE END of the controller
|
||||
initialize();
|
||||
}]
|
||||
|
@ -11,6 +11,13 @@ export default class FormatLib {
|
||||
return Intl.DateTimeFormat().format(moment(date).toDate());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the formatted localized time for the given date
|
||||
*/
|
||||
static time = (date: Date): string => {
|
||||
return Intl.DateTimeFormat(Fablab.intl_locale, { hour: 'numeric', minute: 'numeric' }).format(moment(date).toDate());
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the formatted localized amount for the given price (eg. 20.5 => "20,50 €")
|
||||
*/
|
||||
|
@ -5,10 +5,20 @@ export interface Subscription {
|
||||
plan_id: number,
|
||||
expired_at: Date,
|
||||
canceled_at?: Date,
|
||||
stripe: boolean,
|
||||
plan: Plan
|
||||
}
|
||||
|
||||
export interface SubscriptionRequest {
|
||||
plan_id: number
|
||||
}
|
||||
|
||||
export interface UpdateSubscriptionRequest {
|
||||
id: number,
|
||||
expired_at: Date,
|
||||
free: boolean
|
||||
}
|
||||
|
||||
export interface SubscriptionPaymentDetails {
|
||||
payment_schedule: boolean,
|
||||
card: boolean
|
||||
}
|
||||
|
@ -65,5 +65,6 @@
|
||||
@import "modules/pricing/edit-pack";
|
||||
@import "modules/prepaid-packs/propose-packs-modal";
|
||||
@import "modules/prepaid-packs/packs-summary";
|
||||
@import "modules/subscriptions/free-extend-modal";
|
||||
|
||||
@import "app.responsive";
|
||||
|
@ -0,0 +1,18 @@
|
||||
.free-extend-modal {
|
||||
.fab-modal-content {
|
||||
padding: 30px;
|
||||
|
||||
.configuration-form {
|
||||
padding: 15px;
|
||||
|
||||
.input-wrapper {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.free-days {
|
||||
display: block;
|
||||
@extend .fab-input--input;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -77,8 +77,14 @@
|
||||
{{ 'app.admin.members_edit.price_' | translate }} {{ subscription.plan.amount | currency}}
|
||||
</p>
|
||||
<div ng-hide="user.id === currentUser.id">
|
||||
<button class="btn btn-default" ng-click="updateSubscriptionModal(subscription, true)" translate>{{ 'app.admin.members_edit.offer_free_days' }}</button>
|
||||
<button class="btn btn-default" ng-click="toggleFreeExtendModal()" translate>{{ 'app.admin.members_edit.offer_free_days' }}</button>
|
||||
<button class="btn btn-default" ng-click="updateSubscriptionModal(subscription, false)" translate>{{ 'app.admin.members_edit.renew_subscription' }}</button>
|
||||
<free-extend-modal is-open="isOpenFreeExtendModal"
|
||||
toggle-modal="toggleFreeExtendModal"
|
||||
subscription="subscription"
|
||||
on-error="onError"
|
||||
on-success="onExtendSuccess">
|
||||
</free-extend-modal>
|
||||
</div>
|
||||
<p class="alert alert-info" ng-show="user.id === currentUser.id" translate>
|
||||
{{ 'app.admin.members_edit.cannot_extend_own_subscription' }}
|
||||
|
@ -3,10 +3,6 @@
|
||||
</div>
|
||||
<div class="modal-body m-lg">
|
||||
<div class="alert alert-danger">
|
||||
<div ng-show="free">
|
||||
<p translate>{{ 'app.admin.members_edit.offer_free_days_infos' }}</p>
|
||||
<p translate>{{ 'app.admin.members_edit.credits_will_remain_unchanged' }}</p>
|
||||
</div>
|
||||
<div ng-hide="free">
|
||||
<p translate>{{ 'app.admin.members_edit.renew_subscription_info' }}</p>
|
||||
<p translate>{{ 'app.admin.members_edit.credits_will_be_reset' }}</p>
|
||||
@ -43,10 +39,6 @@
|
||||
<p ng-show="!payment_details.payment_schedule && payment_details.card" translate>{{ 'app.admin.members_edit.one_payment_card' }}</p>
|
||||
<p ng-show="!payment_details.payment_schedule && !payment_details.card" translate>{{ 'app.admin.members_edit.one_payment_check' }}</p>
|
||||
</div>
|
||||
<div class="form-group" ng-show="free">
|
||||
<label translate>{{ 'app.admin.members_edit.number_of_free_days' }}</label>
|
||||
<span class="form-control">{{days}}</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
|
@ -1 +1,3 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.partial! 'api/subscriptions/subscription', subscription: @subscription
|
||||
|
@ -890,13 +890,7 @@ en:
|
||||
no_invoices_for_now: "No invoices for now."
|
||||
current_expiration_date: "Current subscription will expire at:"
|
||||
expiration_date: "Change subscription expiration date"
|
||||
offer_free_days_infos: "You are about to extend the user's subscription by offering him free additional days."
|
||||
credits_will_remain_unchanged: "The balance of free credits (training / machines / spaces) of the user will remain unchanged."
|
||||
renew_subscription_info: "You are about to renew the user's subscription by charging him again for his current subscription."
|
||||
credits_will_be_reset: "The balance of free credits (training / machines / spaces) of the user will be reset, unused credits will be lost."
|
||||
new_subscription_start: "The new subscription will start at:"
|
||||
new_expiration_date: "New expiration date:"
|
||||
number_of_free_days: "Number of free days:"
|
||||
payment_schedule_card: "The previous subscription was charged by card through a payment schedule, this one will be charged the same way. The first deadline will be charged when the current subscription expires, then each following month."
|
||||
payment_schedule_check: "The previous subscription was charged by check through a payment schedule, this one will be charged the same way. Before confirming please ensure you have all the checks to collect all the monthly payments."
|
||||
one_payment_card: "The previous subscription was charged by card through a single payment, this one will be charged the same way. The payment will be charged right now."
|
||||
@ -912,6 +906,21 @@ en:
|
||||
to_credit: 'Credit'
|
||||
cannot_credit_own_wallet: "You cannot credit your own wallet. Please ask another manager or an administrator to credit your wallet."
|
||||
cannot_extend_own_subscription: "You cannot extend your own subscription. Please ask another manager or an administrator to extend your subscription."
|
||||
# extend a subscription for free
|
||||
free_extend_modal:
|
||||
extend_subscription: "Extend the subscription"
|
||||
offer_free_days_infos: "You are about to extend the user's subscription by offering him free additional days."
|
||||
credits_will_remain_unchanged: "The balance of free credits (training / machines / spaces) of the user will remain unchanged."
|
||||
current_expiration: "Current subscription will expire at:"
|
||||
DATE_TIME: "{DATE} {TIME}"
|
||||
new_expiration_date: "New expiration date:"
|
||||
number_of_free_days: "Number of free days:"
|
||||
extend: "Extend"
|
||||
# renew a subscription
|
||||
renew_subscription_modal:
|
||||
renew_subscription: "Renew the subscription"
|
||||
renew_subscription_info: "You are about to renew the user's subscription by charging him again for his current subscription."
|
||||
credits_will_be_reset: "The balance of free credits (training / machines / spaces) of the user will be reset, unused credits will be lost."
|
||||
#add a new administrator to the platform
|
||||
admins_new:
|
||||
add_an_administrator: "Add an administrator"
|
||||
|
Loading…
Reference in New Issue
Block a user