1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2024-11-29 10:24:20 +01:00

move free extend modal to react

This commit is contained in:
Sylvain 2021-10-11 18:50:53 +02:00
parent e646eb8cb5
commit 70f0e21543
15 changed files with 227 additions and 48 deletions

View 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;
}
}

View File

@ -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 (

View File

@ -77,6 +77,7 @@ const PaymentScheduleSummary: React.FC<PaymentScheduleSummaryProps> = ({ schedul
</div>
);
};
const PaymentScheduleSummaryWrapper: React.FC<PaymentScheduleSummaryProps> = ({ schedule }) => {
return (
<Loader>

View File

@ -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")

View File

@ -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>

View File

@ -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']));

View File

@ -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();
}]

View File

@ -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 €")
*/

View File

@ -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
}

View File

@ -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";

View File

@ -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;
}
}
}
}

View File

@ -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' }}

View File

@ -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">

View File

@ -1 +1,3 @@
# frozen_string_literal: true
json.partial! 'api/subscriptions/subscription', subscription: @subscription

View File

@ -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"