1
0
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:
Sylvain 2020-11-24 13:26:15 +01:00
parent b88c1009db
commit 1e5f7ea1fd
17 changed files with 374 additions and 238 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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