mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-01-29 18:52:22 +01:00
WIP: refactor stripe payment modal to react + do not user angular in react component (use Intl instead)
This commit is contained in:
parent
b88c1009db
commit
1e5f7ea1fd
@ -9,6 +9,8 @@
|
||||
- [TODO DEPLOY] `rails fablab:maintenance:rebuild_stylesheet`
|
||||
- [TODO DEPLOY] `rails fablab:stripe:set_product_id`
|
||||
- [TODO DEPLOY] `rails fablab:setup:add_schedule_reference`
|
||||
- [TODO DEPLOY] add the `INTL_LOCALE` environment variable (see [doc/environment.md](doc/environment.md#INTL_LOCALE) for configuration details)
|
||||
- [TODO DEPLOY] add the `INTL_CURRENCY` environment variable (see [doc/environment.md](doc/environment.md#INTL_CURRENCY) for configuration details)
|
||||
|
||||
## v4.6.3 2020 October 28
|
||||
|
||||
|
@ -1,24 +0,0 @@
|
||||
/**
|
||||
* This is a compatibility wrapper to allow usage of stripe.js Elements inside of the angular.js app
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Elements } from '@stripe/react-stripe-js';
|
||||
import { react2angular } from 'react2angular';
|
||||
import { IApplication } from '../../models/application';
|
||||
import SettingAPI from '../../api/setting';
|
||||
import { loadStripe } from "@stripe/stripe-js";
|
||||
|
||||
declare var Application: IApplication;
|
||||
const stripePublicKey = SettingAPI.get('stripe_public_key');
|
||||
|
||||
const ElementsWrapper: React.FC = () => {
|
||||
const publicKey = stripePublicKey.read();
|
||||
const stripePromise = loadStripe(publicKey.value);
|
||||
|
||||
return (
|
||||
<Elements stripe={stripePromise} />
|
||||
);
|
||||
}
|
||||
|
||||
Application.Components.component('stripeElements', react2angular(ElementsWrapper));
|
@ -1,8 +1,8 @@
|
||||
/**
|
||||
* This component is a modal dialog that can wraps the application style
|
||||
* This component is a template for a modal dialog that wraps the application style
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { ReactNode } from 'react';
|
||||
import Modal from 'react-modal';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Loader } from './loader';
|
||||
@ -13,15 +13,23 @@ Modal.setAppElement('body');
|
||||
interface FabModalProps {
|
||||
title: string,
|
||||
isOpen: boolean,
|
||||
toggleModal: () => void
|
||||
toggleModal: () => void,
|
||||
confirmButton?: ReactNode
|
||||
}
|
||||
|
||||
const blackLogoFile = CustomAssetAPI.get('logo-black-file');
|
||||
|
||||
export const FabModal: React.FC<FabModalProps> = ({ title, isOpen, toggleModal, children }) => {
|
||||
export const FabModal: React.FC<FabModalProps> = ({ title, isOpen, toggleModal, children, confirmButton }) => {
|
||||
const { t } = useTranslation('shared');
|
||||
const blackLogo = blackLogoFile.read();
|
||||
|
||||
/**
|
||||
* Check if the confirm button should be present
|
||||
*/
|
||||
const hasConfirmButton = (): boolean => {
|
||||
return confirmButton !== undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen}
|
||||
className="fab-modal"
|
||||
@ -40,7 +48,8 @@ export const FabModal: React.FC<FabModalProps> = ({ title, isOpen, toggleModal,
|
||||
</div>
|
||||
<div className="fab-modal-footer">
|
||||
<Loader>
|
||||
<button className="close-modal-btn" onClick={toggleModal}>{t('app.shared.buttons.close')}</button>
|
||||
<button className="modal-btn--close" onClick={toggleModal}>{t('app.shared.buttons.close')}</button>
|
||||
{hasConfirmButton() && <span className="modal-btn--confirm">{confirmButton}</span>}
|
||||
</Loader>
|
||||
</div>
|
||||
</Modal>
|
||||
|
@ -8,19 +8,19 @@ import { react2angular } from 'react2angular';
|
||||
import moment from 'moment';
|
||||
import { IApplication } from '../models/application';
|
||||
import '../lib/i18n';
|
||||
import { IFilterService } from 'angular';
|
||||
import { PaymentSchedule } from '../models/payment-schedule';
|
||||
import { Loader } from './loader';
|
||||
import { FabModal } from './fab-modal';
|
||||
import { IFablab } from '../models/fablab';
|
||||
|
||||
declare var Application: IApplication;
|
||||
declare var Fablab: IFablab;
|
||||
|
||||
interface PaymentScheduleSummaryProps {
|
||||
schedule: PaymentSchedule,
|
||||
$filter: IFilterService
|
||||
schedule: PaymentSchedule
|
||||
}
|
||||
|
||||
const PaymentScheduleSummary: React.FC<PaymentScheduleSummaryProps> = ({ schedule, $filter }) => {
|
||||
const PaymentScheduleSummary: React.FC<PaymentScheduleSummaryProps> = ({ schedule }) => {
|
||||
const { t } = useTranslation('shared');
|
||||
const [modal, setModal] = useState(false);
|
||||
|
||||
@ -34,7 +34,7 @@ const PaymentScheduleSummary: React.FC<PaymentScheduleSummaryProps> = ({ schedul
|
||||
* Return the formatted localized amount for the given price (eg. 20.5 => "20,50 €")
|
||||
*/
|
||||
const formatPrice = (price: number): string => {
|
||||
return $filter('currency')(price);
|
||||
return new Intl.NumberFormat(Fablab.intl_locale, {style: 'currency', currency: Fablab.intl_currency}).format(price);
|
||||
}
|
||||
/**
|
||||
* Test if all payment deadlines have the same amount
|
||||
@ -90,12 +90,12 @@ const PaymentScheduleSummary: React.FC<PaymentScheduleSummaryProps> = ({ schedul
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const PaymentScheduleSummaryWrapper: React.FC<PaymentScheduleSummaryProps> = ({ schedule, $filter }) => {
|
||||
const PaymentScheduleSummaryWrapper: React.FC<PaymentScheduleSummaryProps> = ({ schedule }) => {
|
||||
return (
|
||||
<Loader>
|
||||
<PaymentScheduleSummary schedule={schedule} $filter={$filter} />
|
||||
<PaymentScheduleSummary schedule={schedule} />
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
|
||||
Application.Components.component('paymentScheduleSummary', react2angular(PaymentScheduleSummaryWrapper, ['schedule'], ['$filter']));
|
||||
Application.Components.component('paymentScheduleSummary', react2angular(PaymentScheduleSummaryWrapper, ['schedule']));
|
||||
|
@ -5,7 +5,6 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { react2angular } from 'react2angular';
|
||||
import { IFilterService } from 'angular';
|
||||
import moment from 'moment';
|
||||
import _ from 'lodash'
|
||||
import { IApplication } from '../models/application';
|
||||
@ -13,8 +12,10 @@ import { Plan } from '../models/plan';
|
||||
import { User, UserRole } from '../models/user';
|
||||
import { Loader } from './loader';
|
||||
import '../lib/i18n';
|
||||
import { IFablab } from '../models/fablab';
|
||||
|
||||
declare var Application: IApplication;
|
||||
declare var Fablab: IFablab;
|
||||
|
||||
interface PlanCardProps {
|
||||
plan: Plan,
|
||||
@ -22,23 +23,22 @@ interface PlanCardProps {
|
||||
operator: User,
|
||||
isSelected: boolean,
|
||||
onSelectPlan: (plan: Plan) => void,
|
||||
$filter: IFilterService
|
||||
}
|
||||
|
||||
const PlanCard: React.FC<PlanCardProps> = ({ plan, user, operator, onSelectPlan, isSelected, $filter }) => {
|
||||
const PlanCard: React.FC<PlanCardProps> = ({ plan, user, operator, onSelectPlan, isSelected }) => {
|
||||
const { t } = useTranslation('public');
|
||||
/**
|
||||
* Return the formatted localized amount of the given plan (eg. 20.5 => "20,50 €")
|
||||
*/
|
||||
const amount = () : string => {
|
||||
return $filter('currency')(plan.amount);
|
||||
return new Intl.NumberFormat(Fablab.intl_locale, {style: 'currency', currency: Fablab.intl_currency}).format(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 $filter('currency')(monthly);
|
||||
return new Intl.NumberFormat(Fablab.intl_locale, {style: 'currency', currency: Fablab.intl_currency}).format(monthly);
|
||||
}
|
||||
/**
|
||||
* Return the formatted localized duration of te given plan (eg. Month/3 => "3 mois")
|
||||
@ -122,12 +122,12 @@ const PlanCard: React.FC<PlanCardProps> = ({ plan, user, operator, onSelectPlan,
|
||||
);
|
||||
}
|
||||
|
||||
const PlanCardWrapper: React.FC<PlanCardProps> = ({ plan, user, operator, onSelectPlan, isSelected, $filter }) => {
|
||||
const PlanCardWrapper: React.FC<PlanCardProps> = ({ plan, user, operator, onSelectPlan, isSelected }) => {
|
||||
return (
|
||||
<Loader>
|
||||
<PlanCard plan={plan} user={user} operator={operator} isSelected={isSelected} onSelectPlan={onSelectPlan} $filter={$filter} />
|
||||
<PlanCard plan={plan} user={user} operator={operator} isSelected={isSelected} onSelectPlan={onSelectPlan}/>
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
|
||||
Application.Components.component('planCard', react2angular(PlanCardWrapper, ['plan', 'user', 'operator', 'onSelectPlan', 'isSelected'], ['$filter']));
|
||||
Application.Components.component('planCard', react2angular(PlanCardWrapper, ['plan', 'user', 'operator', 'onSelectPlan', 'isSelected']));
|
||||
|
@ -1,71 +0,0 @@
|
||||
/**
|
||||
* This component enables the user to type his card data.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { CardElement, useElements, useStripe } from '@stripe/react-stripe-js';
|
||||
import { react2angular } from 'react2angular';
|
||||
import { Loader } from './loader';
|
||||
import { IApplication } from '../models/application';
|
||||
|
||||
|
||||
declare var Application: IApplication;
|
||||
|
||||
const StripeCard: React.FC = () => {
|
||||
|
||||
const stripe = useStripe();
|
||||
const elements = useElements();
|
||||
|
||||
const handleSubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
// Stripe.js has not loaded yet
|
||||
if (!stripe || !elements) { return; }
|
||||
|
||||
const cardElement = elements.getElement(CardElement);
|
||||
|
||||
const { error, paymentMethod } = await stripe.createPaymentMethod({
|
||||
type: 'card',
|
||||
card: cardElement,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.log('[error]', error);
|
||||
} else {
|
||||
console.log('[PaymentMethod]', paymentMethod);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="stripe-card">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<CardElement
|
||||
options={{
|
||||
style: {
|
||||
base: {
|
||||
fontSize: '16px',
|
||||
color: '#424770',
|
||||
'::placeholder': { color: '#aab7c4' }
|
||||
},
|
||||
invalid: {
|
||||
color: '#9e2146',
|
||||
iconColor: '#9e2146'
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const StripeCardWrapper: React.FC = () => {
|
||||
return (
|
||||
<Loader>
|
||||
<StripeCard />
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
|
||||
Application.Components.component('stripeCard', react2angular(StripeCardWrapper));
|
22
app/frontend/src/javascript/components/stripe-elements.tsx
Normal file
22
app/frontend/src/javascript/components/stripe-elements.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
/**
|
||||
* This component initializes the stripe's Elements tag with the API key
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Elements } from '@stripe/react-stripe-js';
|
||||
import { IApplication } from '../models/application';
|
||||
import SettingAPI from '../api/setting';
|
||||
import { loadStripe } from "@stripe/stripe-js";
|
||||
|
||||
const stripePublicKey = SettingAPI.get('stripe_public_key');
|
||||
|
||||
export const StripeElements: React.FC = ({ children }) => {
|
||||
const publicKey = stripePublicKey.read();
|
||||
const stripePromise = loadStripe(publicKey.value);
|
||||
|
||||
return (
|
||||
<Elements stripe={stripePromise}>
|
||||
{children}
|
||||
</Elements>
|
||||
);
|
||||
}
|
134
app/frontend/src/javascript/components/stripe-modal.tsx
Normal file
134
app/frontend/src/javascript/components/stripe-modal.tsx
Normal file
@ -0,0 +1,134 @@
|
||||
/**
|
||||
* This component enables the user to type his card data.
|
||||
*/
|
||||
|
||||
import React, { FormEvent, ReactNode, useState } from 'react';
|
||||
import { CardElement, useElements, useStripe } from '@stripe/react-stripe-js';
|
||||
import { react2angular } from 'react2angular';
|
||||
import { Loader } from './loader';
|
||||
import { IApplication } from '../models/application';
|
||||
import { StripeElements } from './stripe-elements';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FabModal } from './fab-modal';
|
||||
import { PaymentMethod } from '@stripe/stripe-js';
|
||||
import { WalletInfo } from './wallet-info';
|
||||
import { Reservation } from '../models/reservation';
|
||||
import { User } from '../models/user';
|
||||
import { Wallet } from '../models/wallet';
|
||||
|
||||
declare var Application: IApplication;
|
||||
|
||||
interface StripeModalProps {
|
||||
isOpen: boolean,
|
||||
toggleModal: () => void,
|
||||
afterSuccess: (paymentMethod: PaymentMethod) => void,
|
||||
reservation: Reservation,
|
||||
currentUser: User,
|
||||
wallet: Wallet,
|
||||
price: number,
|
||||
remainingPrice: number,
|
||||
}
|
||||
|
||||
const StripeModal: React.FC<StripeModalProps> = ({ isOpen, toggleModal, afterSuccess, reservation, currentUser, wallet, price, remainingPrice }) => {
|
||||
|
||||
const stripe = useStripe();
|
||||
const elements = useElements();
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
const [errors, setErrors] = useState(null);
|
||||
const [submitState, setSubmitState] = useState(false);
|
||||
|
||||
/**
|
||||
* Handle the submission of the form. Depending on the configuration, it will create the payment method on stripe,
|
||||
* or it will process a payment with the inputted card.
|
||||
*/
|
||||
const handleSubmit = async (event: FormEvent): Promise<void> => {
|
||||
event.preventDefault();
|
||||
|
||||
// Stripe.js has not loaded yet
|
||||
if (!stripe || !elements) { return; }
|
||||
|
||||
const cardElement = elements.getElement(CardElement);
|
||||
const { error, paymentMethod } = await stripe.createPaymentMethod({
|
||||
type: 'card',
|
||||
card: cardElement,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
setErrors(error.message);
|
||||
} else {
|
||||
setErrors(null);
|
||||
afterSuccess(paymentMethod);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there is currently an error to display
|
||||
*/
|
||||
const hasErrors = (): boolean => {
|
||||
return errors !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the state of the submit button: enabled/disabled
|
||||
*/
|
||||
const toggleSubmitButton = (): void => {
|
||||
setSubmitState(!submitState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the form submission button. This button will be shown into the modal footer
|
||||
*/
|
||||
const submitButton = (): ReactNode => {
|
||||
return (
|
||||
<button type="submit"
|
||||
onClick={toggleSubmitButton}
|
||||
disabled={submitState}
|
||||
form="stripe-form"
|
||||
className="validate-btn">
|
||||
{t('app.shared.buttons.confirm')}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="stripe-modal">
|
||||
<FabModal title={t('app.shared.stripe.online_payment')} isOpen={isOpen} toggleModal={toggleModal} confirmButton={submitButton()}>
|
||||
<StripeElements>
|
||||
<form onSubmit={handleSubmit} id="stripe-form">
|
||||
<WalletInfo reservation={reservation} currentUser={currentUser} wallet={wallet} price={price} remainingPrice={remainingPrice} />
|
||||
<CardElement
|
||||
options={{
|
||||
style: {
|
||||
base: {
|
||||
fontSize: '16px',
|
||||
color: '#424770',
|
||||
'::placeholder': { color: '#aab7c4' }
|
||||
},
|
||||
invalid: {
|
||||
color: '#9e2146',
|
||||
iconColor: '#9e2146'
|
||||
},
|
||||
},
|
||||
hidePostalCode: true
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
</StripeElements>
|
||||
{hasErrors() && <div className="stripe-errors">
|
||||
{errors}
|
||||
</div>}
|
||||
</FabModal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const StripeModalWrapper: React.FC<StripeModalProps> = ({ isOpen, toggleModal, afterSuccess, reservation, currentUser, wallet, price, remainingPrice }) => {
|
||||
return (
|
||||
<Loader>
|
||||
<StripeModal isOpen={isOpen} toggleModal={toggleModal} afterSuccess={afterSuccess} reservation={reservation} currentUser={currentUser} wallet={wallet} price={price} remainingPrice={remainingPrice} />
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
|
||||
Application.Components.component('stripeModal', react2angular(StripeModalWrapper, ['isOpen', 'toggleModal', 'afterSuccess', 'reservation', 'currentUser', 'wallet', 'price', 'remainingPrice']));
|
@ -7,117 +7,117 @@ import { useTranslation } from 'react-i18next';
|
||||
import { react2angular } from 'react2angular';
|
||||
import { IApplication } from '../models/application';
|
||||
import '../lib/i18n';
|
||||
import { IFilterService } from 'angular';
|
||||
import { Loader } from './loader';
|
||||
import { Reservation } from '../models/reservation';
|
||||
import { User } from '../models/user';
|
||||
import { Wallet } from '../models/wallet';
|
||||
import { IFablab } from '../models/fablab';
|
||||
|
||||
declare var Application: IApplication;
|
||||
declare var Fablab: IFablab;
|
||||
|
||||
interface WalletInfoProps {
|
||||
reservation: Reservation,
|
||||
$filter: IFilterService,
|
||||
currentUser: User,
|
||||
wallet: Wallet,
|
||||
price: number,
|
||||
remainingPrice: number,
|
||||
reservation: Reservation,
|
||||
currentUser: User,
|
||||
wallet: Wallet,
|
||||
price: number,
|
||||
remainingPrice: number,
|
||||
}
|
||||
|
||||
const WalletInfo: React.FC<WalletInfoProps> = ({ reservation, currentUser, wallet, price, remainingPrice, $filter }) => {
|
||||
const { t } = useTranslation('shared');
|
||||
export const WalletInfo: React.FC<WalletInfoProps> = ({reservation, currentUser, wallet, price, remainingPrice}) => {
|
||||
const {t} = useTranslation('shared');
|
||||
|
||||
/**
|
||||
* Return the formatted localized amount for the given price (eg. 20.5 => "20,50 €")
|
||||
*/
|
||||
const formatPrice = (price: number): string => {
|
||||
return $filter('currency')(price);
|
||||
}
|
||||
/**
|
||||
* Check if the currently connected used is also the person making the reservation.
|
||||
* If the currently connected user (ie. the operator), is an admin or a manager, he may book the reservation for someone else.
|
||||
*/
|
||||
const isOperatorAndClient = (): boolean => {
|
||||
return currentUser.id == reservation.user_id;
|
||||
}
|
||||
/**
|
||||
* If the client has some money in his wallet & the price is not zero, then we should display this component.
|
||||
*/
|
||||
const shouldBeShown = (): boolean => {
|
||||
return wallet.amount > 0 && price > 0;
|
||||
}
|
||||
/**
|
||||
* If the amount in the wallet is not enough to cover the whole price, then the user must pay the remaining price
|
||||
* using another payment mean.
|
||||
*/
|
||||
const hasRemainingPrice = (): boolean => {
|
||||
return remainingPrice > 0;
|
||||
}
|
||||
/**
|
||||
* Does the current cart contains a payment schedule?
|
||||
*/
|
||||
const isPaymentSchedule = (): boolean => {
|
||||
return reservation.plan_id && reservation.payment_schedule;
|
||||
}
|
||||
/**
|
||||
* Return the human-readable name of the item currently bought with the wallet
|
||||
*/
|
||||
const getPriceItem = (): string => {
|
||||
let item = 'other';
|
||||
if (reservation.slots_attributes.length > 0) {
|
||||
item = 'reservation';
|
||||
} else if (reservation.plan_id) {
|
||||
if (reservation.payment_schedule) {
|
||||
item = 'first_deadline';
|
||||
}
|
||||
else item = 'subscription';
|
||||
}
|
||||
|
||||
return t(`app.shared.wallet.wallet_info.item_${item}`);
|
||||
/**
|
||||
* Return the formatted localized amount for the given price (eg. 20.5 => "20,50 €")
|
||||
*/
|
||||
const formatPrice = (price: number): string => {
|
||||
return new Intl.NumberFormat(Fablab.intl_locale, {style: 'currency', currency: Fablab.intl_currency}).format(price);
|
||||
}
|
||||
/**
|
||||
* Check if the currently connected used is also the person making the reservation.
|
||||
* If the currently connected user (ie. the operator), is an admin or a manager, he may book the reservation for someone else.
|
||||
*/
|
||||
const isOperatorAndClient = (): boolean => {
|
||||
return currentUser.id == reservation.user_id;
|
||||
}
|
||||
/**
|
||||
* If the client has some money in his wallet & the price is not zero, then we should display this component.
|
||||
*/
|
||||
const shouldBeShown = (): boolean => {
|
||||
return wallet.amount > 0 && price > 0;
|
||||
}
|
||||
/**
|
||||
* If the amount in the wallet is not enough to cover the whole price, then the user must pay the remaining price
|
||||
* using another payment mean.
|
||||
*/
|
||||
const hasRemainingPrice = (): boolean => {
|
||||
return remainingPrice > 0;
|
||||
}
|
||||
/**
|
||||
* Does the current cart contains a payment schedule?
|
||||
*/
|
||||
const isPaymentSchedule = (): boolean => {
|
||||
return reservation.plan_id && reservation.payment_schedule;
|
||||
}
|
||||
/**
|
||||
* Return the human-readable name of the item currently bought with the wallet
|
||||
*/
|
||||
const getPriceItem = (): string => {
|
||||
let item = 'other';
|
||||
if (reservation.slots_attributes.length > 0) {
|
||||
item = 'reservation';
|
||||
} else if (reservation.plan_id) {
|
||||
if (reservation.payment_schedule) {
|
||||
item = 'first_deadline';
|
||||
} else item = 'subscription';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="wallet-info">
|
||||
{shouldBeShown() && <div>
|
||||
{isOperatorAndClient() && <div>
|
||||
<h3>{t('app.shared.wallet.wallet_info.you_have_AMOUNT_in_wallet', {AMOUNT : formatPrice(wallet.amount)})}</h3>
|
||||
{!hasRemainingPrice() &&<p>
|
||||
{t('app.shared.wallet.wallet_info.wallet_pay_ITEM', {ITEM: getPriceItem()})}
|
||||
</p>}
|
||||
{hasRemainingPrice() &&<p>
|
||||
{t('app.shared.wallet.wallet_info.credit_AMOUNT_for_pay_ITEM', {
|
||||
AMOUNT: formatPrice(remainingPrice),
|
||||
ITEM: getPriceItem()
|
||||
})}
|
||||
</p>}
|
||||
</div>}
|
||||
{!isOperatorAndClient() && <div>
|
||||
<h3>{t('app.shared.wallet.wallet_info.client_have_AMOUNT_in_wallet', {AMOUNT : formatPrice(wallet.amount)})}</h3>
|
||||
{!hasRemainingPrice() &&<p>
|
||||
{t('app.shared.wallet.wallet_info.client_wallet_pay_ITEM', {ITEM: getPriceItem()})}
|
||||
</p>}
|
||||
{hasRemainingPrice() &&<p>
|
||||
{t('app.shared.wallet.wallet_info.client_credit_AMOUNT_for_pay_ITEM', {
|
||||
AMOUNT: formatPrice(remainingPrice),
|
||||
ITEM: getPriceItem()
|
||||
})}
|
||||
</p>}
|
||||
</div>}
|
||||
{!hasRemainingPrice() && isPaymentSchedule() &&<p className="info-deadlines">
|
||||
<i className="fa fa-warning" />
|
||||
<span>{t('app.shared.wallet.wallet_info.other_deadlines_no_wallet')}</span>
|
||||
</p>}
|
||||
</div>}
|
||||
</div>
|
||||
);
|
||||
return t(`app.shared.wallet.wallet_info.item_${item}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="wallet-info">
|
||||
{shouldBeShown() && <div>
|
||||
{isOperatorAndClient() && <div>
|
||||
<h3>{t('app.shared.wallet.wallet_info.you_have_AMOUNT_in_wallet', {AMOUNT: formatPrice(wallet.amount)})}</h3>
|
||||
{!hasRemainingPrice() && <p>
|
||||
{t('app.shared.wallet.wallet_info.wallet_pay_ITEM', {ITEM: getPriceItem()})}
|
||||
</p>}
|
||||
{hasRemainingPrice() && <p>
|
||||
{t('app.shared.wallet.wallet_info.credit_AMOUNT_for_pay_ITEM', {
|
||||
AMOUNT: formatPrice(remainingPrice),
|
||||
ITEM: getPriceItem()
|
||||
})}
|
||||
</p>}
|
||||
</div>}
|
||||
{!isOperatorAndClient() && <div>
|
||||
<h3>{t('app.shared.wallet.wallet_info.client_have_AMOUNT_in_wallet', {AMOUNT: formatPrice(wallet.amount)})}</h3>
|
||||
{!hasRemainingPrice() && <p>
|
||||
{t('app.shared.wallet.wallet_info.client_wallet_pay_ITEM', {ITEM: getPriceItem()})}
|
||||
</p>}
|
||||
{hasRemainingPrice() && <p>
|
||||
{t('app.shared.wallet.wallet_info.client_credit_AMOUNT_for_pay_ITEM', {
|
||||
AMOUNT: formatPrice(remainingPrice),
|
||||
ITEM: getPriceItem()
|
||||
})}
|
||||
</p>}
|
||||
</div>}
|
||||
{!hasRemainingPrice() && isPaymentSchedule() && <p className="info-deadlines">
|
||||
<i className="fa fa-warning"/>
|
||||
<span>{t('app.shared.wallet.wallet_info.other_deadlines_no_wallet')}</span>
|
||||
</p>}
|
||||
</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const WalletInfoWrapper: React.FC<WalletInfoProps> = ({ currentUser, reservation, $filter, price, remainingPrice, wallet }) => {
|
||||
return (
|
||||
<Loader>
|
||||
<WalletInfo currentUser={currentUser} reservation={reservation} $filter={$filter} price={price} remainingPrice={remainingPrice} wallet={wallet} />
|
||||
</Loader>
|
||||
);
|
||||
const WalletInfoWrapper: React.FC<WalletInfoProps> = ({currentUser, reservation, price, remainingPrice, wallet}) => {
|
||||
return (
|
||||
<Loader>
|
||||
<WalletInfo currentUser={currentUser} reservation={reservation} price={price}
|
||||
remainingPrice={remainingPrice} wallet={wallet}/>
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
|
||||
Application.Components.component('walletInfo', react2angular(WalletInfoWrapper, ['currentUser', 'price', 'remainingPrice', 'reservation', 'wallet'], ['$filter']));
|
||||
Application.Components.component('walletInfo', react2angular(WalletInfoWrapper, ['currentUser', 'price', 'remainingPrice', 'reservation', 'wallet']));
|
||||
|
29
app/frontend/src/javascript/models/fablab.ts
Normal file
29
app/frontend/src/javascript/models/fablab.ts
Normal file
@ -0,0 +1,29 @@
|
||||
export interface IFablab {
|
||||
plansModule: boolean,
|
||||
spacesModule: boolean,
|
||||
walletModule: boolean,
|
||||
statisticsModule: boolean,
|
||||
defaultHost: string,
|
||||
trackingId: string,
|
||||
superadminId: number,
|
||||
baseHostUrl: string,
|
||||
locale: string,
|
||||
moment_locale: string,
|
||||
summernote_locale: string,
|
||||
fullcalendar_locale: string,
|
||||
intl_locale: string,
|
||||
intl_currency: string,
|
||||
timezone: string,
|
||||
weekStartingDay: string,
|
||||
d3DateFormat: string,
|
||||
uibDateFormat: string,
|
||||
sessionTours: Array<string>,
|
||||
translations: {
|
||||
app: {
|
||||
shared: {
|
||||
buttons: Object,
|
||||
messages: Object,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -62,9 +62,7 @@
|
||||
text-align: right;
|
||||
border-top: 1px solid #e5e5e5;
|
||||
|
||||
.close-modal-btn {
|
||||
color: black;
|
||||
background-color: #fbfbfb;
|
||||
.modal-btn {
|
||||
margin-bottom: 0;
|
||||
margin-left: 5px;
|
||||
display: inline-block;
|
||||
@ -75,14 +73,24 @@
|
||||
touch-action: manipulation;
|
||||
cursor: pointer;
|
||||
background-image: none;
|
||||
border: 1px solid #c9c9c9;
|
||||
padding: 6px 12px;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background-color: #f2f2f2;
|
||||
&--close {
|
||||
@extend .modal-btn;
|
||||
color: black;
|
||||
background-color: #fbfbfb;
|
||||
border: 1px solid #c9c9c9;
|
||||
|
||||
&:hover {
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
}
|
||||
|
||||
&--confirm {
|
||||
@extend .modal-btn;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -37,6 +37,8 @@
|
||||
Fablab.moment_locale = "<%= Rails.application.secrets.moment_locale %>";
|
||||
Fablab.summernote_locale = "<%= Rails.application.secrets.summernote_locale %>";
|
||||
Fablab.fullcalendar_locale = "<%= Rails.application.secrets.fullcalendar_locale %>";
|
||||
Fablab.intl_locale = "<%= Rails.application.secrets.intl_locale %>";
|
||||
Fablab.intl_currency = "<%= Rails.application.secrets.intl_currency %>";
|
||||
Fablab.timezone = "<%= Time.zone.tzinfo.name %>";
|
||||
Fablab.translations = {
|
||||
app: {
|
||||
@ -88,31 +90,29 @@
|
||||
|
||||
<%= flash_messages %>
|
||||
|
||||
<stripe-elements>
|
||||
<section class="vbox">
|
||||
<section class="vbox">
|
||||
|
||||
<header class="header header-md navbar navbar-fixed-top-xs">
|
||||
<div ui-view="header"></div>
|
||||
</header>
|
||||
<header class="header header-md navbar navbar-fixed-top-xs">
|
||||
<div ui-view="header"></div>
|
||||
</header>
|
||||
|
||||
<section ui-view="content">
|
||||
<section class="hbox stretch">
|
||||
<aside id="nav" class="aside-md bg-red hidden-print" ui-view="leftnav"></aside>
|
||||
<section ui-view="content">
|
||||
<section class="hbox stretch">
|
||||
<aside id="nav" class="aside-md bg-red hidden-print" ui-view="leftnav"></aside>
|
||||
|
||||
<section id="content">
|
||||
<section class="vbox">
|
||||
<section id="cookies-modal" ui-view="cookies">
|
||||
</section>
|
||||
<section id="content-main" class="scrollable" ui-view="main">
|
||||
</section>
|
||||
<section id="content">
|
||||
<section class="vbox">
|
||||
<section id="cookies-modal" ui-view="cookies">
|
||||
</section>
|
||||
<section id="content-main" class="scrollable" ui-view="main">
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
</section> <!-- /.hbox -->
|
||||
</section>
|
||||
</section> <!-- /.hbox -->
|
||||
</section>
|
||||
|
||||
</section> <!-- /.vbox -->
|
||||
</stripe-elements>
|
||||
</section> <!-- /.vbox -->
|
||||
|
||||
<div class="app-generator">
|
||||
<span class="app-version" uib-tooltip="{{'app.public.common.version' | translate}} {{version.current}}" ng-if="currentUser && currentUser.role == 'admin'" ng-click="versionModal()">
|
||||
|
@ -26,6 +26,8 @@ development:
|
||||
moment_locale: <%= ENV["MOMENT_LOCALE"] %>
|
||||
summernote_locale: <%= ENV["SUMMERNOTE_LOCALE"] %>
|
||||
angular_locale: <%= ENV["ANGULAR_LOCALE"] %>
|
||||
intl_locale: <%= ENV["INTL_LOCALE"] %>
|
||||
intl_currency: <%= ENV["INTL_CURRENCY"] %>
|
||||
fullcalendar_locale: <%= ENV["FULLCALENDAR_LOCALE"] %>
|
||||
postgresql_language_analyzer: <%= ENV.fetch("POSTGRESQL_LANGUAGE_ANALYZER", 'simple') %>
|
||||
openlab_base_uri: <%= ENV["OPENLAB_BASE_URI"] %>
|
||||
@ -54,6 +56,8 @@ test:
|
||||
moment_locale: en
|
||||
summernote_locale: en-US
|
||||
angular_locale: en-us
|
||||
intl_locale: en-US
|
||||
intl_currency: USD
|
||||
fullcalendar_locale: en
|
||||
postgresql_language_analyzer: french
|
||||
openlab_base_uri:
|
||||
@ -90,6 +94,8 @@ staging:
|
||||
moment_locale: <%= ENV["MOMENT_LOCALE"] %>
|
||||
summernote_locale: <%= ENV["SUMMERNOTE_LOCALE"] %>
|
||||
angular_locale: <%= ENV["ANGULAR_LOCALE"] %>
|
||||
intl_locale: <%= ENV["INTL_LOCALE"] %>
|
||||
intl_currency: <%= ENV["INTL_CURRENCY"] %>
|
||||
fullcalendar_locale: <%= ENV["FULLCALENDAR_LOCALE"] %>
|
||||
postgresql_language_analyzer: <%= ENV.fetch("POSTGRESQL_LANGUAGE_ANALYZER", 'simple') %>
|
||||
openlab_base_uri: <%= ENV["OPENLAB_BASE_URI"] %>
|
||||
@ -129,6 +135,8 @@ production:
|
||||
moment_locale: <%= ENV["MOMENT_LOCALE"] %>
|
||||
summernote_locale: <%= ENV["SUMMERNOTE_LOCALE"] %>
|
||||
angular_locale: <%= ENV["ANGULAR_LOCALE"] %>
|
||||
intl_locale: <%= ENV["INTL_LOCALE"] %>
|
||||
intl_currency: <%= ENV["INTL_CURRENCY"] %>
|
||||
fullcalendar_locale: <%= ENV["FULLCALENDAR_LOCALE"] %>
|
||||
postgresql_language_analyzer: <%= ENV.fetch("POSTGRESQL_LANGUAGE_ANALYZER", 'simple') %>
|
||||
openlab_base_uri: <%= ENV["OPENLAB_BASE_URI"] %>
|
||||
|
@ -195,6 +195,21 @@ See [code.angularjs.org/i18n/angular-locale_*.js](https://code.angularjs.org/1.8
|
||||
Configure the fullCalendar JS agenda library.
|
||||
|
||||
See [github.com/fullcalendar/fullcalendar/lang/*.js](https://github.com/fullcalendar/fullcalendar/tree/v3.10.2/locale) for a list of available locales. Default is **en-us**.
|
||||
<a name="INTL_LOCALE"></a>
|
||||
|
||||
INTL_LOCALE
|
||||
|
||||
Configure the locale for the javascript Intl Object.
|
||||
This locale must be a Unicode BCP 47 locale identifier.
|
||||
See [Intl - Javascript | MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl#Locale_identification_and_negotiation) for more info about configuring this setting.
|
||||
<a name="INTL_CURRENCY"></a>
|
||||
|
||||
INTL_CURRENCY
|
||||
|
||||
Configure the currency for the javascript Intl Object.
|
||||
Possible values are the ISO 4217 currency codes, such as "USD" for the US dollar, "EUR" for the euro.
|
||||
See [Current currency & funds code list](http://www.currency-iso.org/en/home/tables/table-a1.html) for a list of available values.
|
||||
There is no default value; this setting MUST be provided.
|
||||
<a name="POSTGRESQL_LANGUAGE_ANALYZER"></a>
|
||||
|
||||
POSTGRESQL_LANGUAGE_ANALYZER
|
||||
|
@ -38,6 +38,8 @@ MOMENT_LOCALE=fr
|
||||
SUMMERNOTE_LOCALE=fr-FR
|
||||
ANGULAR_LOCALE=fr-fr
|
||||
FULLCALENDAR_LOCALE=fr
|
||||
INTL_LOCALE=fr-FR
|
||||
INTL_CURRENCY=EUR
|
||||
FORCE_VERSION_CHECK=false
|
||||
ALLOW_INSECURE_HTTP=false
|
||||
|
||||
|
@ -26,6 +26,8 @@ MOMENT_LOCALE=fr
|
||||
SUMMERNOTE_LOCALE=fr-FR
|
||||
ANGULAR_LOCALE=fr-fr
|
||||
FULLCALENDAR_LOCALE=fr
|
||||
INTL_LOCALE=fr-FR
|
||||
INTL_CURRENCY=EUR
|
||||
|
||||
POSTGRESQL_LANGUAGE_ANALYZER=french
|
||||
|
||||
|
@ -236,7 +236,7 @@ configure_env_file()
|
||||
doc=$(\curl -sSL https://raw.githubusercontent.com/sleede/fab-manager/master/doc/environment.md)
|
||||
variables=(DEFAULT_HOST DEFAULT_PROTOCOL DELIVERY_METHOD SMTP_ADDRESS SMTP_PORT SMTP_USER_NAME SMTP_PASSWORD SMTP_AUTHENTICATION \
|
||||
SMTP_ENABLE_STARTTLS_AUTO SMTP_OPENSSL_VERIFY_MODE SMTP_TLS LOG_LEVEL MAX_IMAGE_SIZE MAX_CAO_SIZE MAX_IMPORT_SIZE DISK_SPACE_MB_ALERT \
|
||||
SUPERADMIN_EMAIL APP_LOCALE RAILS_LOCALE MOMENT_LOCALE SUMMERNOTE_LOCALE ANGULAR_LOCALE FULLCALENDAR_LOCALE \
|
||||
SUPERADMIN_EMAIL APP_LOCALE RAILS_LOCALE MOMENT_LOCALE SUMMERNOTE_LOCALE ANGULAR_LOCALE FULLCALENDAR_LOCALE INTL_LOCALE INTL_CURRENCY\
|
||||
POSTGRESQL_LANGUAGE_ANALYZER TIME_ZONE WEEK_STARTING_DAY D3_DATE_FORMAT UIB_DATE_FORMAT EXCEL_DATE_FORMAT)
|
||||
for variable in "${variables[@]}"; do
|
||||
local var_doc current
|
||||
|
Loading…
x
Reference in New Issue
Block a user