From 70f0e2154382a505300bf2ef7e61bc3355666fd2 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Mon, 11 Oct 2021 18:50:53 +0200 Subject: [PATCH] move free extend modal to react --- .../src/javascript/api/subscription.ts | 20 ++++ .../machines/pending-training-modal.tsx | 9 +- .../payment-schedule-summary.tsx | 1 + .../javascript/components/plans/plan-card.tsx | 8 +- .../components/pricing/machines-pricing.tsx | 7 +- .../subscriptions/free-extend-modal.tsx | 112 ++++++++++++++++++ .../javascript/controllers/admin/members.js | 41 ++++--- app/frontend/src/javascript/lib/format.ts | 7 ++ .../src/javascript/models/subscription.ts | 12 +- app/frontend/src/stylesheets/application.scss | 1 + .../subscriptions/free-extend-modal.scss | 18 +++ .../templates/admin/members/edit.html | 8 +- .../admin/subscriptions/expired_at_modal.html | 8 -- .../api/subscriptions/show.json.jbuilder | 2 + config/locales/app.admin.en.yml | 21 +++- 15 files changed, 227 insertions(+), 48 deletions(-) create mode 100644 app/frontend/src/javascript/api/subscription.ts create mode 100644 app/frontend/src/javascript/components/subscriptions/free-extend-modal.tsx create mode 100644 app/frontend/src/stylesheets/modules/subscriptions/free-extend-modal.scss diff --git a/app/frontend/src/javascript/api/subscription.ts b/app/frontend/src/javascript/api/subscription.ts new file mode 100644 index 000000000..3a18cc539 --- /dev/null +++ b/app/frontend/src/javascript/api/subscription.ts @@ -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 { + const res: AxiosResponse = await apiClient.patch(`/api/subscriptions/${request.id}`, { subscription: request }); + return res?.data; + } + + static async get (id: number): Promise { + const res: AxiosResponse = await apiClient.get(`/api/subscriptions/${id}`); + return res?.data; + } + + static async paymentsDetails (id: number): Promise { + const res: AxiosResponse = await apiClient.get(`/api/subscriptions/${id}/payment_details`); + return res?.data; + } +} diff --git a/app/frontend/src/javascript/components/machines/pending-training-modal.tsx b/app/frontend/src/javascript/components/machines/pending-training-modal.tsx index 3c732b321..e1f32fd2a 100644 --- a/app/frontend/src/javascript/components/machines/pending-training-modal.tsx +++ b/app/frontend/src/javascript/components/machines/pending-training-modal.tsx @@ -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 = ({ 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 ( diff --git a/app/frontend/src/javascript/components/payment-schedule/payment-schedule-summary.tsx b/app/frontend/src/javascript/components/payment-schedule/payment-schedule-summary.tsx index 5ac79dfd9..84800ec60 100644 --- a/app/frontend/src/javascript/components/payment-schedule/payment-schedule-summary.tsx +++ b/app/frontend/src/javascript/components/payment-schedule/payment-schedule-summary.tsx @@ -77,6 +77,7 @@ const PaymentScheduleSummary: React.FC = ({ schedul ); }; + const PaymentScheduleSummaryWrapper: React.FC = ({ schedule }) => { return ( diff --git a/app/frontend/src/javascript/components/plans/plan-card.tsx b/app/frontend/src/javascript/components/plans/plan-card.tsx index 509ec8709..244e099fd 100644 --- a/app/frontend/src/javascript/components/plans/plan-card.tsx +++ b/app/frontend/src/javascript/components/plans/plan-card.tsx @@ -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 = ({ 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") diff --git a/app/frontend/src/javascript/components/pricing/machines-pricing.tsx b/app/frontend/src/javascript/components/pricing/machines-pricing.tsx index 4356913d7..cfde86d70 100644 --- a/app/frontend/src/javascript/components/pricing/machines-pricing.tsx +++ b/app/frontend/src/javascript/components/pricing/machines-pricing.tsx @@ -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 = ({ 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 = ({ onError, onSuccess })

-

+

{t('app.admin.machines_pricing.you_can_override')}

diff --git a/app/frontend/src/javascript/components/subscriptions/free-extend-modal.tsx b/app/frontend/src/javascript/components/subscriptions/free-extend-modal.tsx new file mode 100644 index 000000000..9f5b057f9 --- /dev/null +++ b/app/frontend/src/javascript/components/subscriptions/free-extend-modal.tsx @@ -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 = ({ isOpen, toggleModal, subscription, onError, onSuccess }) => { + const { t } = useTranslation('admin'); + + const [expirationDate, setExpirationDate] = useState(new Date(subscription.expired_at)); + const [freeDays, setFreeDays] = useState(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 ( + + +

{t('app.admin.free_extend_modal.offer_free_days_infos')}

+

{t('app.admin.free_extend_modal.credits_will_remain_unchanged')}

+
+
+ + + + + + + +
+ ); +}; + +const FreeExtendModalWrapper: React.FC = ({ toggleModal, subscription, isOpen, onSuccess, onError }) => { + return ( + + + + ); +}; + +Application.Components.component('freeExtendModal', react2angular(FreeExtendModalWrapper, ['toggleModal', 'subscription', 'isOpen', 'onError', 'onSuccess'])); diff --git a/app/frontend/src/javascript/controllers/admin/members.js b/app/frontend/src/javascript/controllers/admin/members.js index 7a37fb3c3..c7b1281b0 100644 --- a/app/frontend/src/javascript/controllers/admin/members.js +++ b/app/frontend/src/javascript/controllers/admin/members.js @@ -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(); }] diff --git a/app/frontend/src/javascript/lib/format.ts b/app/frontend/src/javascript/lib/format.ts index 1b0cda093..8ebb31aa7 100644 --- a/app/frontend/src/javascript/lib/format.ts +++ b/app/frontend/src/javascript/lib/format.ts @@ -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 €") */ diff --git a/app/frontend/src/javascript/models/subscription.ts b/app/frontend/src/javascript/models/subscription.ts index c6148f7a9..d975c7727 100644 --- a/app/frontend/src/javascript/models/subscription.ts +++ b/app/frontend/src/javascript/models/subscription.ts @@ -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 +} diff --git a/app/frontend/src/stylesheets/application.scss b/app/frontend/src/stylesheets/application.scss index 7f9c99754..84c0cee14 100644 --- a/app/frontend/src/stylesheets/application.scss +++ b/app/frontend/src/stylesheets/application.scss @@ -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"; diff --git a/app/frontend/src/stylesheets/modules/subscriptions/free-extend-modal.scss b/app/frontend/src/stylesheets/modules/subscriptions/free-extend-modal.scss new file mode 100644 index 000000000..17a681cc0 --- /dev/null +++ b/app/frontend/src/stylesheets/modules/subscriptions/free-extend-modal.scss @@ -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; + } + } + } +} diff --git a/app/frontend/templates/admin/members/edit.html b/app/frontend/templates/admin/members/edit.html index 78e8ea5ce..55298bd8b 100644 --- a/app/frontend/templates/admin/members/edit.html +++ b/app/frontend/templates/admin/members/edit.html @@ -77,8 +77,14 @@ {{ 'app.admin.members_edit.price_' | translate }} {{ subscription.plan.amount | currency}}

- + + +

{{ 'app.admin.members_edit.cannot_extend_own_subscription' }} diff --git a/app/frontend/templates/admin/subscriptions/expired_at_modal.html b/app/frontend/templates/admin/subscriptions/expired_at_modal.html index 7e94d692e..38bf7b632 100644 --- a/app/frontend/templates/admin/subscriptions/expired_at_modal.html +++ b/app/frontend/templates/admin/subscriptions/expired_at_modal.html @@ -3,10 +3,6 @@