1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-29 18:52:22 +01:00

Merge branch 'dev' for release 5.1.1

This commit is contained in:
Du Peng 2021-07-01 16:20:39 +02:00
commit 0b33297f9a
120 changed files with 1645 additions and 7675 deletions

View File

@ -1,3 +1,4 @@
node_modules/**
vendor/**
public/**

View File

@ -1,10 +1,12 @@
{
"root": true,
"extends": [
"standard",
"plugin:react/recommended"
],
"rules": {
"semi": ["error", "always"]
"semi": ["error", "always"],
"no-use-before-define": "off"
},
"globals": {
"Application": true,
@ -13,6 +15,39 @@
"moment": true,
"_": true
},
"plugins": ["lint-erb"]
"plugins": ["lint-erb"],
"overrides": [
{
"files": ["**/*.ts", "**/*.tsx"],
"env": { "browser": true, "es6": true },
"extends": [
"plugin:react/recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaFeatures": { "jsx": true },
"ecmaVersion": 2018,
"sourceType": "module",
"project": "./tsconfig.json"
},
"settings": {
"react": {
"version": "detect"
}
},
"plugins": ["@typescript-eslint", "react"],
"rules": {
"react/prop-types": "off"
}
},
{
"files": ["app/frontend/src/javascript/models/**/*.ts"],
"rules": {
"camelcase": "off"
}
}
]
}

View File

@ -1,5 +1,11 @@
# Changelog Fab-manager
## v5.1.1 2021 July 1st
- Set up the linter for Typescript files
- Disable SQL format for the schema
- Fix a bug: config norwegian locale
## v5.1.0 2021 July 1st
- Prepaid packs of hours for the machines
@ -37,7 +43,7 @@
- Updated chokidar to 3.5.2
- Updated codemirror to 5.62.0
- Updated convert-source-map to 1.8.0
- Updated core-js-compat to 3.15.0
- Updated core-js-compat to 3.15.0
- Updated electron-to-chromium to 1.3.752
- Updated immer to 9.0.3
- Updated jquery-ujs to 1.2.3
@ -70,8 +76,8 @@
- Do not display the type in the plans list
- Updated medium-editor to v5 and angular-medium-editor accordingly
- Fix a bug: a message tells that creating a new plan fails, but it worked
- Fix a bug: unable to select no category in plan creation/edition after a category selection
- Fix a bug: the training validation modal shows cancelled trainings
- Fix a bug: unable to select no category in plan creation/edition after a category selection
- Fix a bug: the training validation modal shows cancelled trainings
- [TODO DEPLOY] `rails db:seed`
## v5.0.3 2021 June 14
@ -760,7 +766,7 @@
## v4.0.4 2019 August 14
- Fix a bug: #140 VAT rate is erroneous in invoices.
Note: this bug was introduced in v4.0.3 and requires (if you are on v4.0.3) to regenerate the invoices since August 1st
Note: this bug was introduced in v4.0.3 and requires (if you are on v4.0.3) to regenerate the invoices since August 1st
- [TODO DEPLOY] `rake fablab:maintenance:regenerate_invoices[2019,8]`
## v4.0.3 2019 August 01

View File

@ -1,4 +1,6 @@
import axios, { AxiosInstance } from 'axios'
import axios, { AxiosInstance } from 'axios';
type Error = { error: string };
const token: HTMLMetaElement = document.querySelector('[name="csrf-token"]');
const client: AxiosInstance = axios.create({
@ -21,7 +23,7 @@ client.interceptors.response.use(function (response) {
return Promise.reject(extractHumanReadableMessage(message));
});
function extractHumanReadableMessage(error: any): string {
function extractHumanReadableMessage (error: string|Error): string {
if (typeof error === 'string') {
if (error.match(/^<!DOCTYPE html>/)) {
// parse ruby error pages
@ -40,7 +42,7 @@ function extractHumanReadableMessage(error: any): string {
let message = '';
if (error instanceof Object) {
// API errors
if (error.hasOwnProperty('error') && typeof error.error === 'string') {
if (Object.prototype.hasOwnProperty.call(error, 'error') && typeof error.error === 'string') {
return error.error;
}
// iterate through all the keys to build the message

View File

@ -1,6 +1,6 @@
import axios, { AxiosInstance } from 'axios'
import axios, { AxiosInstance } from 'axios';
function client(key: string): AxiosInstance {
function client (key: string): AxiosInstance {
return axios.create({
baseURL: 'https://api.stripe.com/v1/',
headers: {
@ -12,4 +12,3 @@ function client(key: string): AxiosInstance {
}
export default client;

View File

@ -8,4 +8,3 @@ export default class CustomAssetAPI {
return res?.data?.custom_asset;
}
}

View File

@ -4,7 +4,7 @@ import { EventTheme } from '../models/event-theme';
export default class EventThemeAPI {
static async index (): Promise<Array<EventTheme>> {
const res: AxiosResponse<Array<EventTheme>> = await apiClient.get(`/api/event_themes`);
const res: AxiosResponse<Array<EventTheme>> = await apiClient.get('/api/event_themes');
return res?.data;
}
}

View File

@ -1,11 +1,12 @@
import stripeClient from '../clients/stripe-client';
import { AxiosResponse } from 'axios';
import { ListCharges, PIIToken } from '../../models/stripe';
export default class StripeAPI {
/**
* @see https://stripe.com/docs/api/tokens/create_pii
*/
static async createPIIToken(key: string, piiId: string): Promise<any> {
static async createPIIToken (key: string, piiId: string): Promise<PIIToken> {
const params = new URLSearchParams();
params.append('pii[id_number]', piiId);
@ -13,17 +14,17 @@ export default class StripeAPI {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}
};
const res: AxiosResponse = await stripeClient(key).post('tokens', params, config);
const res: AxiosResponse<PIIToken> = await stripeClient(key).post('tokens', params, config);
return res?.data;
}
/**
* @see https://stripe.com/docs/api/charges/list
*/
static async listAllCharges(key: string): Promise<any> {
const res: AxiosResponse = await stripeClient(key).get('charges');
static async listAllCharges (key: string): Promise<ListCharges> {
const res: AxiosResponse<ListCharges> = await stripeClient(key).get('charges');
return res?.data;
}
}

View File

@ -8,10 +8,9 @@ export default class GroupAPI {
return res?.data;
}
private static filtersToQuery(filters?: GroupIndexFilter): string {
private static filtersToQuery (filters?: GroupIndexFilter): string {
if (!filters) return '';
return '?' + Object.entries(filters).map(f => `${f[0]}=${f[1]}`).join('&');
}
}

View File

@ -5,9 +5,8 @@ import { PaymentSchedule } from '../models/payment-schedule';
import { Invoice } from '../models/invoice';
export default class LocalPaymentAPI {
static async confirmPayment (cart_items: ShoppingCart): Promise<PaymentSchedule|Invoice> {
const res: AxiosResponse<PaymentSchedule|Invoice> = await apiClient.post('/api/local_payment/confirm_payment', cart_items);
static async confirmPayment (cartItems: ShoppingCart): Promise<PaymentSchedule|Invoice> {
const res: AxiosResponse<PaymentSchedule|Invoice> = await apiClient.post('/api/local_payment/confirm_payment', cartItems);
return res?.data;
}
}

View File

@ -13,10 +13,9 @@ export default class MachineAPI {
return res?.data;
}
private static filtersToQuery(filters?: MachineIndexFilter): string {
private static filtersToQuery (filters?: MachineIndexFilter): string {
if (!filters) return '';
return '?' + Object.entries(filters).map(f => `${f[0]}=${f[1]}`).join('&');
}
}

View File

@ -9,7 +9,7 @@ import {
export default class PaymentScheduleAPI {
static async list (query: PaymentScheduleIndexRequest): Promise<Array<PaymentSchedule>> {
const res: AxiosResponse = await apiClient.post(`/api/payment_schedules/list`, query);
const res: AxiosResponse = await apiClient.post('/api/payment_schedules/list', query);
return res?.data;
}
@ -18,17 +18,17 @@ export default class PaymentScheduleAPI {
return res?.data;
}
static async cashCheck(paymentScheduleItemId: number): Promise<CashCheckResponse> {
static async cashCheck (paymentScheduleItemId: number): Promise<CashCheckResponse> {
const res: AxiosResponse = await apiClient.post(`/api/payment_schedules/items/${paymentScheduleItemId}/cash_check`);
return res?.data;
}
static async refreshItem(paymentScheduleItemId: number): Promise<RefreshItemResponse> {
static async refreshItem (paymentScheduleItemId: number): Promise<RefreshItemResponse> {
const res: AxiosResponse = await apiClient.post(`/api/payment_schedules/items/${paymentScheduleItemId}/refresh_item`);
return res?.data;
}
static async payItem(paymentScheduleItemId: number): Promise<PayItemResponse> {
static async payItem (paymentScheduleItemId: number): Promise<PayItemResponse> {
const res: AxiosResponse = await apiClient.post(`/api/payment_schedules/items/${paymentScheduleItemId}/pay_item`);
return res?.data;
}
@ -38,4 +38,3 @@ export default class PaymentScheduleAPI {
return res?.data;
}
}

View File

@ -1,6 +1,6 @@
import apiClient from './clients/api-client';
import { AxiosResponse } from 'axios';
import { ShoppingCart, UpdateCardResponse } from '../models/payment';
import { ShoppingCart } from '../models/payment';
import { User } from '../models/user';
import {
CheckHashResponse,
@ -12,39 +12,38 @@ import { Invoice } from '../models/invoice';
import { PaymentSchedule } from '../models/payment-schedule';
export default class PayzenAPI {
static async chargeSDKTest(baseURL: string, username: string, password: string): Promise<SdkTestResponse> {
static async chargeSDKTest (baseURL: string, username: string, password: string): Promise<SdkTestResponse> {
const res: AxiosResponse<SdkTestResponse> = await apiClient.post('/api/payzen/sdk_test', { base_url: baseURL, username, password });
return res?.data;
}
static async chargeCreatePayment(cart: ShoppingCart, customer: User): Promise<CreatePaymentResponse> {
static async chargeCreatePayment (cart: ShoppingCart, customer: User): Promise<CreatePaymentResponse> {
const res: AxiosResponse<CreatePaymentResponse> = await apiClient.post('/api/payzen/create_payment', { cart_items: cart, customer_id: customer.id });
return res?.data;
}
static async chargeCreateToken(cart: ShoppingCart, customer: User): Promise<CreateTokenResponse> {
const res: AxiosResponse = await apiClient.post('/api/payzen/create_token', { cart_items: cart, customer_id: customer.id });
static async chargeCreateToken (cart: ShoppingCart, customer: User): Promise<CreateTokenResponse> {
const res: AxiosResponse = await apiClient.post('/api/payzen/create_token', { cart_items: cart, customer_id: customer.id });
return res?.data;
}
static async checkHash(algorithm: string, hashKey: string, hash: string, data: string): Promise<CheckHashResponse> {
static async checkHash (algorithm: string, hashKey: string, hash: string, data: string): Promise<CheckHashResponse> {
const res: AxiosResponse<CheckHashResponse> = await apiClient.post('/api/payzen/check_hash', { algorithm, hash_key: hashKey, hash, data });
return res?.data;
}
static async confirm(orderId: string, cart: ShoppingCart): Promise<Invoice> {
static async confirm (orderId: string, cart: ShoppingCart): Promise<Invoice> {
const res: AxiosResponse<Invoice> = await apiClient.post('/api/payzen/confirm_payment', { cart_items: cart, order_id: orderId });
return res?.data;
}
static async confirmPaymentSchedule(orderId: string, transactionUuid: string, cart: ShoppingCart): Promise<PaymentSchedule> {
static async confirmPaymentSchedule (orderId: string, transactionUuid: string, cart: ShoppingCart): Promise<PaymentSchedule> {
const res: AxiosResponse<PaymentSchedule> = await apiClient.post('/api/payzen/confirm_payment_schedule', { cart_items: cart, order_id: orderId, transaction_uuid: transactionUuid });
return res?.data;
}
static async updateToken(payment_schedule_id: number): Promise<CreateTokenResponse> {
const res: AxiosResponse<CreateTokenResponse> = await apiClient.post(`/api/payzen/update_token`, { payment_schedule_id });
static async updateToken (paymentScheduleId: number): Promise<CreateTokenResponse> {
const res: AxiosResponse<CreateTokenResponse> = await apiClient.post('/api/payzen/update_token', { payment_schedule_id: paymentScheduleId });
return res?.data;
}
}

View File

@ -23,4 +23,3 @@ export default class PlanCategoryAPI {
return res?.data;
}
}

View File

@ -9,8 +9,7 @@ export default class PlanAPI {
}
static async durations (): Promise<Array<PlansDuration>> {
const res: AxiosResponse<Array<PlansDuration>> = await apiClient.get('/api/plans/durations');
const res: AxiosResponse<Array<PlansDuration>> = await apiClient.get('/api/plans/durations');
return res?.data;
}
}

View File

@ -28,11 +28,9 @@ export default class PrepaidPackAPI {
return res?.data;
}
private static filtersToQuery(filters?: PackIndexFilter): string {
private static filtersToQuery (filters?: PackIndexFilter): string {
if (!filters) return '';
return '?' + Object.entries(filters).map(f => `${f[0]}=${f[1]}`).join('&');
}
}

View File

@ -5,7 +5,7 @@ import { ComputePriceResult, Price, PriceIndexFilter } from '../models/price';
export default class PriceAPI {
static async compute (cart: ShoppingCart): Promise<ComputePriceResult> {
const res: AxiosResponse<ComputePriceResult> = await apiClient.post(`/api/prices/compute`, cart);
const res: AxiosResponse<ComputePriceResult> = await apiClient.post('/api/prices/compute', cart);
return res?.data;
}
@ -16,13 +16,12 @@ export default class PriceAPI {
static async update (price: Price): Promise<Price> {
const res: AxiosResponse<Price> = await apiClient.patch(`/api/prices/${price.id}`, { price });
return res?.data;
return res?.data;
}
private static filtersToQuery(filters?: PriceIndexFilter): string {
private static filtersToQuery (filters?: PriceIndexFilter): string {
if (!filters) return '';
return '?' + Object.entries(filters).map(f => `${f[0]}=${f[1]}`).join('&');
}
}

View File

@ -1,6 +1,6 @@
import apiClient from './clients/api-client';
import { AxiosResponse } from 'axios';
import { Setting, SettingBulkResult, SettingError, SettingName } from '../models/setting';
import { Setting, SettingBulkResult, SettingError, SettingName, SettingValue } from '../models/setting';
export default class SettingAPI {
static async get (name: SettingName): Promise<Setting> {
@ -8,7 +8,7 @@ export default class SettingAPI {
return res?.data?.setting;
}
static async query (names: Array<SettingName>): Promise<Map<SettingName, any>> {
static async query (names: Array<SettingName>): Promise<Map<SettingName, string>> {
const params = new URLSearchParams();
params.append('names', `['${names.join("','")}']`);
@ -16,13 +16,13 @@ export default class SettingAPI {
return SettingAPI.toSettingsMap(names, res?.data);
}
static async update (name: SettingName, value: any): Promise<Setting> {
static async update (name: SettingName, value: SettingValue): Promise<Setting> {
const res: AxiosResponse = await apiClient.patch(`/api/settings/${name}`, { setting: { value } });
if (res.status === 304) { return { name, value }; }
return res?.data?.setting;
if (res.status === 304) { return { name, value: `${value}` }; }
return res?.data?.setting;
}
static async bulkUpdate (settings: Map<SettingName, any>, transactional: boolean = false): Promise<Map<SettingName, SettingBulkResult>> {
static async bulkUpdate (settings: Map<SettingName, SettingValue>, transactional = false): Promise<Map<SettingName, SettingBulkResult>> {
const res: AxiosResponse = await apiClient.patch(`/api/settings/bulk_update?transactional=${transactional}`, { settings: SettingAPI.toObjectArray(settings) });
return SettingAPI.toBulkMap(res?.data?.settings);
}
@ -32,7 +32,7 @@ export default class SettingAPI {
return res?.data?.isPresent;
}
private static toSettingsMap(names: Array<SettingName>, data: Object): Map<SettingName, any> {
private static toSettingsMap (names: Array<SettingName>, data: Record<string, string|null>): Map<SettingName, string> {
const map = new Map();
names.forEach(name => {
map.set(name, data[name] || '');
@ -40,7 +40,7 @@ export default class SettingAPI {
return map;
}
private static toBulkMap(data: Array<Setting|SettingError>): Map<SettingName, SettingBulkResult> {
private static toBulkMap (data: Array<Setting|SettingError>): Map<SettingName, SettingBulkResult> {
const map = new Map();
data.forEach(item => {
const itemData: SettingBulkResult = { status: true };
@ -55,20 +55,19 @@ export default class SettingAPI {
itemData.localized = item.localized;
}
map.set(item.name as SettingName, itemData)
map.set(item.name as SettingName, itemData);
});
return map;
}
private static toObjectArray(data: Map<SettingName, any>): Array<Object> {
private static toObjectArray (data: Map<SettingName, SettingValue>): Array<Record<string, SettingValue>> {
const array = [];
data.forEach((value, key) => {
array.push({
name: key,
value
})
});
});
return array;
}
}

View File

@ -5,42 +5,41 @@ import { PaymentSchedule } from '../models/payment-schedule';
import { Invoice } from '../models/invoice';
export default class StripeAPI {
static async confirmMethod (stp_payment_method_id: string, cart_items: ShoppingCart): Promise<PaymentConfirmation|Invoice> {
const res: AxiosResponse<PaymentConfirmation|Invoice> = await apiClient.post(`/api/stripe/confirm_payment`, {
payment_method_id: stp_payment_method_id,
cart_items
static async confirmMethod (paymentMethodId: string, cartItems: ShoppingCart): Promise<PaymentConfirmation|Invoice> {
const res: AxiosResponse<PaymentConfirmation|Invoice> = await apiClient.post('/api/stripe/confirm_payment', {
payment_method_id: paymentMethodId,
cart_items: cartItems
});
return res?.data;
}
static async confirmIntent (stp_payment_intent_id: string, cart_items: ShoppingCart): Promise<PaymentConfirmation|Invoice> {
const res: AxiosResponse = await apiClient.post(`/api/payments/confirm_payment`, {
payment_intent_id: stp_payment_intent_id,
cart_items
static async confirmIntent (paymentMethodId: string, cartItems: ShoppingCart): Promise<PaymentConfirmation|Invoice> {
const res: AxiosResponse = await apiClient.post('/api/payments/confirm_payment', {
payment_intent_id: paymentMethodId,
cart_items: cartItems
});
return res?.data;
}
static async setupIntent (user_id: number): Promise<IntentConfirmation> {
const res: AxiosResponse<IntentConfirmation> = await apiClient.get(`/api/stripe/setup_intent/${user_id}`);
static async setupIntent (userId: number): Promise<IntentConfirmation> {
const res: AxiosResponse<IntentConfirmation> = await apiClient.get(`/api/stripe/setup_intent/${userId}`);
return res?.data;
}
static async confirmPaymentSchedule (setup_intent_id: string, cart_items: ShoppingCart): Promise<PaymentSchedule> {
const res: AxiosResponse<PaymentSchedule> = await apiClient.post(`/api/stripe/confirm_payment_schedule`, {
setup_intent_id,
cart_items
static async confirmPaymentSchedule (setupIntentId: string, cartItems: ShoppingCart): Promise<PaymentSchedule> {
const res: AxiosResponse<PaymentSchedule> = await apiClient.post('/api/stripe/confirm_payment_schedule', {
setup_intent_id: setupIntentId,
cart_items: cartItems
});
return res?.data;
}
static async updateCard (user_id: number, stp_payment_method_id: string, payment_schedule_id?: number): Promise<UpdateCardResponse> {
const res: AxiosResponse<UpdateCardResponse> = await apiClient.post(`/api/stripe/update_card`, {
user_id,
payment_method_id: stp_payment_method_id,
payment_schedule_id
static async updateCard (userId: number, paymentMethodId: string, paymentScheduleId?: number): Promise<UpdateCardResponse> {
const res: AxiosResponse<UpdateCardResponse> = await apiClient.post('/api/stripe/update_card', {
user_id: userId,
payment_method_id: paymentMethodId,
payment_schedule_id: paymentScheduleId
});
return res?.data;
}
}

View File

@ -4,8 +4,7 @@ import { Theme } from '../models/theme';
export default class ThemeAPI {
static async index (): Promise<Array<Theme>> {
const res: AxiosResponse<Array<Theme>> = await apiClient.get(`/api/themes`);
const res: AxiosResponse<Array<Theme>> = await apiClient.get('/api/themes');
return res?.data;
}
}

View File

@ -3,12 +3,12 @@ import { UserPack, UserPackIndexFilter } from '../models/user-pack';
import { AxiosResponse } from 'axios';
export default class UserPackAPI {
static async index(filters: UserPackIndexFilter): Promise<Array<UserPack>> {
static async index (filters: UserPackIndexFilter): Promise<Array<UserPack>> {
const res: AxiosResponse<Array<UserPack>> = await apiClient.get(`/api/user_packs${this.filtersToQuery(filters)}`);
return res?.data;
}
private static filtersToQuery(filters?: UserPackIndexFilter): string {
private static filtersToQuery (filters?: UserPackIndexFilter): string {
if (!filters) return '';
return '?' + Object.entries(filters).map(f => `${f[0]}=${f[1]}`).join('&');

View File

@ -3,9 +3,8 @@ import { AxiosResponse } from 'axios';
import { Wallet } from '../models/wallet';
export default class WalletAPI {
static async getByUser (user_id: number): Promise<Wallet> {
const res: AxiosResponse = await apiClient.get(`/api/wallet/by_user/${user_id}`);
static async getByUser (userId: number): Promise<Wallet> {
const res: AxiosResponse = await apiClient.get(`/api/wallet/by_user/${userId}`);
return res?.data;
}
}

View File

@ -5,7 +5,6 @@
* creating namespaces and moduled for controllers, filters, services, and directives.
*/
// eslint-disable-next-line no-use-before-define
var Application = Application || {};
Application.Components = angular.module('application.components', []);

View File

@ -5,6 +5,6 @@ import Switch from 'react-switch';
import { react2angular } from 'react2angular';
import { IApplication } from '../../models/application';
declare var Application: IApplication;
declare const Application: IApplication;
Application.Components.component('switch', react2angular(Switch, ['checked', 'onChange', 'id', 'className', 'disabled']));

View File

@ -10,8 +10,8 @@ interface FabAlertProps {
*/
export const FabAlert: React.FC<FabAlertProps> = ({ level, className, children }) => {
return (
<div className={`fab-alert fab-alert--${level} ${className ? className : ''}`}>
<div className={`fab-alert fab-alert--${level} ${className || ''}`}>
{children}
</div>
)
);
};

View File

@ -18,14 +18,14 @@ export const FabButton: React.FC<FabButtonProps> = ({ onClick, icon, className,
*/
const hasIcon = (): boolean => {
return !!icon;
}
};
/**
* Check if the current button has children properties (like some text)
*/
const hasChildren = (): boolean => {
return !!children;
}
};
/**
* Handle the action of the button
@ -34,15 +34,14 @@ export const FabButton: React.FC<FabButtonProps> = ({ onClick, icon, className,
if (typeof onClick === 'function') {
onClick(e);
}
}
};
return (
<button type={type} form={form} onClick={handleClick} disabled={disabled} className={`fab-button ${className ? className : ''}`}>
<button type={type} form={form} onClick={handleClick} disabled={disabled} className={`fab-button ${className || ''}`}>
{hasIcon() && <span className={hasChildren() ? 'fab-button--icon' : 'fab-button--icon-only'}>{icon}</span>}
{children}
</button>
);
}
};
FabButton.defaultProps = { type: 'button' };

View File

@ -1,10 +1,12 @@
import React, { BaseSyntheticEvent, ReactNode, useCallback, useEffect, useState } from 'react';
import { debounce as _debounce } from 'lodash';
type inputType = string|number|readonly string [];
interface FabInputProps {
id: string,
onChange?: (value: string, validity?: ValidityState) => void,
defaultValue: any,
onChange?: (value: inputType, validity?: ValidityState) => void,
defaultValue: inputType,
icon?: ReactNode,
addOn?: ReactNode,
addOnClassName?: string,
@ -27,7 +29,7 @@ interface FabInputProps {
* This component is a template for an input component that wraps the application style
*/
export const FabInput: React.FC<FabInputProps> = ({ id, onChange, defaultValue, icon, className, disabled, type, required, debounce, addOn, addOnClassName, readOnly, maxLength, pattern, placeholder, error, step, min, max }) => {
const [inputValue, setInputValue] = useState<any>(defaultValue);
const [inputValue, setInputValue] = useState<inputType>(defaultValue);
/**
* When the component is mounted, initialize the default value for the input.
@ -47,21 +49,21 @@ export const FabInput: React.FC<FabInputProps> = ({ id, onChange, defaultValue,
*/
const hasIcon = (): boolean => {
return !!icon;
}
};
/**
* Check if the current component was provided an add-on element to display, at the end of the input
*/
const hasAddOn = (): boolean => {
return !!addOn;
}
};
/**
* Check if the current component was provided an error string to display, on the input
*/
const hasError = (): boolean => {
return !!error;
}
};
/**
* Debounced (ie. temporised) version of the 'on change' callback.
@ -81,31 +83,31 @@ export const FabInput: React.FC<FabInputProps> = ({ id, onChange, defaultValue,
onChange(value, validity);
}
}
}
};
return (
<div className={`fab-input ${className ? className : ''}`}>
<div className={`fab-input ${className || ''}`}>
<div className={`input-wrapper ${hasError() ? 'input-error' : ''}`}>
{hasIcon() && <span className="fab-input--icon">{icon}</span>}
<input id={id}
type={type}
step={step}
min={min}
max={max}
className="fab-input--input"
value={inputValue}
onChange={handleChange}
disabled={disabled}
required={required}
readOnly={readOnly}
maxLength={maxLength}
pattern={pattern}
placeholder={placeholder} />
{hasAddOn() && <span className={`fab-input--addon ${addOnClassName ? addOnClassName : ''}`}>{addOn}</span>}
type={type}
step={step}
min={min}
max={max}
className="fab-input--input"
value={inputValue}
onChange={handleChange}
disabled={disabled}
required={required}
readOnly={readOnly}
maxLength={maxLength}
pattern={pattern}
placeholder={placeholder} />
{hasAddOn() && <span className={`fab-input--addon ${addOnClassName || ''}`}>{addOn}</span>}
</div>
{hasError() && <span className="fab-input--error">{error}</span> }
</div>
);
}
};
FabInput.defaultProps = { type: 'text', debounce: 0 };

View File

@ -54,46 +54,46 @@ export const FabModal: React.FC<FabModalProps> = ({ title, isOpen, toggleModal,
*/
const hasConfirmButton = (): boolean => {
return confirmButton !== undefined;
}
};
/**
* Check if the behavior of the confirm button is to send a form, using the provided ID
*/
const confirmationSendForm = (): boolean => {
return onConfirmSendFormId !== undefined;
}
};
/**
* Should we display the close button?
*/
const hasCloseButton = (): boolean => {
return closeButton;
}
};
/**
* Check if there's a custom footer
*/
const hasCustomFooter = (): boolean => {
return customFooter !== undefined;
}
};
/**
* Check if there's a custom header
*/
const hasCustomHeader = (): boolean => {
return customHeader !== undefined;
}
};
return (
<Modal isOpen={isOpen}
className={`fab-modal fab-modal-${width} ${className}`}
overlayClassName="fab-modal-overlay"
onRequestClose={toggleModal}>
className={`fab-modal fab-modal-${width} ${className}`}
overlayClassName="fab-modal-overlay"
onRequestClose={toggleModal}>
<div className="fab-modal-header">
<Loader>
{blackLogo && <img src={blackLogo.custom_asset_file_attributes.attachment_url}
alt={blackLogo.custom_asset_file_attributes.attachment}
className="modal-logo" />}
alt={blackLogo.custom_asset_file_attributes.attachment}
className="modal-logo" />}
</Loader>
{!hasCustomHeader() && <h1>{ title }</h1>}
{hasCustomHeader() && customHeader}
@ -103,7 +103,7 @@ export const FabModal: React.FC<FabModalProps> = ({ title, isOpen, toggleModal,
</div>
<div className="fab-modal-footer">
<Loader>
{hasCloseButton() &&<FabButton className="modal-btn--close" onClick={toggleModal}>{t('app.shared.buttons.close')}</FabButton>}
{hasCloseButton() && <FabButton className="modal-btn--close" onClick={toggleModal}>{t('app.shared.buttons.close')}</FabButton>}
{hasConfirmButton() && !confirmationSendForm() && <FabButton className="modal-btn--confirm" disabled={preventConfirm} onClick={onConfirm}>{confirmButton}</FabButton>}
{hasConfirmButton() && confirmationSendForm() && <FabButton className="modal-btn--confirm" disabled={preventConfirm} type="submit" form={onConfirmSendFormId}>{confirmButton}</FabButton>}
{hasCustomFooter() && customFooter}
@ -111,5 +111,4 @@ export const FabModal: React.FC<FabModalProps> = ({ title, isOpen, toggleModal,
</div>
</Modal>
);
}
};

View File

@ -10,16 +10,15 @@ interface FabPopoverProps {
* This component is a template for a popovers (bottom) that wraps the application style
*/
export const FabPopover: React.FC<FabPopoverProps> = ({ title, className, headerButton, children }) => {
/**
* Check if the header button should be present
*/
const hasHeaderButton = (): boolean => {
return !!headerButton;
}
};
return (
<div className={`fab-popover ${className ? className : ''}`}>
<div className={`fab-popover ${className || ''}`}>
<div className="popover-title">
<h3>{title}</h3>
{hasHeaderButton() && headerButton}
@ -29,4 +28,4 @@ export const FabPopover: React.FC<FabPopoverProps> = ({ title, className, header
</div>
</div>
);
}
};

View File

@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
interface HtmlTranslateProps {
trKey: string,
options?: any
options?: Record<string, string>
}
/**
@ -13,6 +13,6 @@ export const HtmlTranslate: React.FC<HtmlTranslateProps> = ({ trKey, options })
const { t } = useTranslation(trKey?.split('.')[1]);
return (
<span dangerouslySetInnerHTML={{__html: t(trKey, options)}} />
<span dangerouslySetInnerHTML={{ __html: t(trKey, options) }} />
);
}
};

View File

@ -1,10 +1,12 @@
import React, { BaseSyntheticEvent, ReactNode } from 'react';
type inputType = string|number|readonly string [];
interface LabelledInputProps {
id: string,
type?: 'text' | 'date' | 'password' | 'url' | 'time' | 'tel' | 'search' | 'number' | 'month' | 'email' | 'datetime-local' | 'week',
label: string | ReactNode,
value: any,
value: inputType,
onChange: (event: BaseSyntheticEvent) => void
}
@ -18,4 +20,4 @@ export const LabelledInput: React.FC<LabelledInputProps> = ({ id, type, label, v
<input className="input" id={id} type={type} value={value} onChange={onChange} />
</div>
);
}
};

View File

@ -3,7 +3,7 @@ import React, { Suspense } from 'react';
/**
* This component is a wrapper that display a loader while the children components have their rendering suspended
*/
export const Loader: React.FC = ({children }) => {
export const Loader: React.FC = ({ children }) => {
const loading = (
<div className="fa-3x">
<i className="fas fa-circle-notch fa-spin" />
@ -11,8 +11,7 @@ export const Loader: React.FC = ({children }) => {
);
return (
<Suspense fallback={loading}>
{children}
{children}
</Suspense>
);
}
};

View File

@ -24,21 +24,21 @@ export const DocumentFilters: React.FC<DocumentFiltersProps> = ({ onFilterChange
*/
useEffect(() => {
onFilterChange({ reference: referenceFilter, customer: customerFilter, date: dateFilter });
}, [referenceFilter, customerFilter, dateFilter])
}, [referenceFilter, customerFilter, dateFilter]);
/**
* Callback triggered when the input 'reference' is updated.
*/
const handleReferenceUpdate = (e) => {
setReferenceFilter(e.target.value);
}
};
/**
* Callback triggered when the input 'customer' is updated.
*/
const handleCustomerUpdate = (e) => {
setCustomerFilter(e.target.value);
}
};
/**
* Callback triggered when the input 'date' is updated.
@ -47,25 +47,25 @@ export const DocumentFilters: React.FC<DocumentFiltersProps> = ({ onFilterChange
let date = e.target.value;
if (e.target.value === '') date = null;
setDateFilter(date);
}
};
return (
<div className="document-filters">
<LabelledInput id="reference"
label={t('app.admin.invoices.document_filters.reference')}
type="text"
onChange={handleReferenceUpdate}
value={referenceFilter} />
label={t('app.admin.invoices.document_filters.reference')}
type="text"
onChange={handleReferenceUpdate}
value={referenceFilter} />
<LabelledInput id="customer"
label={t('app.admin.invoices.document_filters.customer')}
type="text"
onChange={handleCustomerUpdate}
value={customerFilter} />
label={t('app.admin.invoices.document_filters.customer')}
type="text"
onChange={handleCustomerUpdate}
value={customerFilter} />
<LabelledInput id="reference"
label={t('app.admin.invoices.document_filters.date')}
type="date"
onChange={handleDateUpdate}
value={dateFilter ? dateFilter : ''} />
label={t('app.admin.invoices.document_filters.date')}
type="date"
onChange={handleDateUpdate}
value={dateFilter || ''} />
</div>
);
}
};

View File

@ -8,7 +8,7 @@ import { EventTheme } from '../models/event-theme';
import { IApplication } from '../models/application';
import EventThemeAPI from '../api/event-theme';
declare var Application: IApplication;
declare const Application: IApplication;
interface EventThemesProps {
event: Event,
@ -51,7 +51,7 @@ const EventThemes: React.FC<EventThemesProps> = ({ event, onChange }) => {
}
});
return res;
}
};
/**
* Callback triggered when the selection has changed.
@ -61,18 +61,18 @@ const EventThemes: React.FC<EventThemesProps> = ({ event, onChange }) => {
const res = [];
selectedOptions.forEach(opt => {
res.push(themes.find(t => t.id === opt.value));
})
});
onChange(res);
}
};
/**
* Convert all themes to the react-select format
*/
const buildOptions = (): Array<selectOption> => {
return themes.map(t => {
return { value: t.id, label: t.name }
return { value: t.id, label: t.name };
});
}
};
return (
<div className="event-themes">
@ -80,15 +80,15 @@ const EventThemes: React.FC<EventThemesProps> = ({ event, onChange }) => {
<h3>{ t('app.shared.event.event_themes') }</h3>
<div className="content">
<Select defaultValue={defaultValues()}
placeholder={t('app.shared.event.select_theme')}
onChange={handleChange}
options={buildOptions()}
isMulti />
placeholder={t('app.shared.event.select_theme')}
onChange={handleChange}
options={buildOptions()}
isMulti />
</div>
</div>}
</div>
);
}
};
const EventThemesWrapper: React.FC<EventThemesProps> = ({ event, onChange }) => {
return (
@ -96,7 +96,6 @@ const EventThemesWrapper: React.FC<EventThemesProps> = ({ event, onChange }) =>
<EventThemes event={event} onChange={onChange}/>
</Loader>
);
}
};
Application.Components.component('eventThemes', react2angular(EventThemesWrapper, ['event', 'onChange']));

View File

@ -31,13 +31,13 @@ const MachineCardComponent: React.FC<MachineCardProps> = ({ user, machine, onSho
*/
const handleReserveMachine = (): void => {
onReserveMachine(machine);
}
};
/**
* Callback triggered when the user clicks on the 'view' button
*/
const handleShowMachine = (): void => {
onShowMachine(machine);
}
};
const machinePicture = (): ReactNode => {
if (!machine.machine_image) {
@ -46,26 +46,26 @@ const MachineCardComponent: React.FC<MachineCardProps> = ({ user, machine, onSho
return (
<div className="machine-picture" style={{ backgroundImage: `url(${machine.machine_image})` }} onClick={handleShowMachine} />
)
}
);
};
return (
<div className={`machine-card ${loading ? 'loading' : ''} ${machine.disabled ? 'disabled': ''}`}>
<div className={`machine-card ${loading ? 'loading' : ''} ${machine.disabled ? 'disabled' : ''}`}>
{machinePicture()}
<div className="machine-name">
{machine.name}
</div>
<div className="machine-actions">
{!machine.disabled && <ReserveButton currentUser={user}
machineId={machine.id}
onLoadingStart={() => setLoading(true)}
onLoadingEnd={() => setLoading(false)}
onError={onError}
onSuccess={onSuccess}
onReserveMachine={handleReserveMachine}
onLoginRequested={onLoginRequested}
onEnrollRequested={onEnrollRequested}
className="reserve-button">
machineId={machine.id}
onLoadingStart={() => setLoading(true)}
onLoadingEnd={() => setLoading(false)}
onError={onError}
onSuccess={onSuccess}
onReserveMachine={handleReserveMachine}
onLoginRequested={onLoginRequested}
onEnrollRequested={onEnrollRequested}
className="reserve-button">
<i className="fas fa-bookmark" />
{t('app.public.machine_card.book')}
</ReserveButton>}
@ -78,8 +78,7 @@ const MachineCardComponent: React.FC<MachineCardProps> = ({ user, machine, onSho
</div>
</div>
);
}
};
export const MachineCard: React.FC<MachineCardProps> = ({ user, machine, onShowMachine, onReserveMachine, onError, onSuccess, onLoginRequested, onEnrollRequested }) => {
return (
@ -87,4 +86,4 @@ export const MachineCard: React.FC<MachineCardProps> = ({ user, machine, onShowM
<MachineCardComponent user={user} machine={machine} onShowMachine={onShowMachine} onReserveMachine={onReserveMachine} onError={onError} onSuccess={onSuccess} onLoginRequested={onLoginRequested} onEnrollRequested={onEnrollRequested} />
</Loader>
);
}
};

View File

@ -24,27 +24,27 @@ export const MachinesFilters: React.FC<MachinesFiltersProps> = ({ onStatusSelect
return [
defaultValue,
{ value: false, label: t('app.public.machines_filters.status_disabled') },
{ value: null, label: t('app.public.machines_filters.status_all') },
]
}
{ value: null, label: t('app.public.machines_filters.status_all') }
];
};
/**
* Callback triggered when the user selects a machine status in the dropdown list
*/
const handleStatusSelected = (option: selectOption): void => {
onStatusSelected(option.value);
}
};
return (
<div className="machines-filters">
<div className="status-filter">
<label htmlFor="status">{t('app.public.machines_filters.show_machines')}</label>
<Select defaultValue={defaultValue}
id="status"
className="status-select"
onChange={handleStatusSelected}
options={buildBooleanOptions()}/>
id="status"
className="status-select"
onChange={handleStatusSelected}
options={buildBooleanOptions()}/>
</div>
</div>
)
}
);
};

View File

@ -8,7 +8,7 @@ import { MachineCard } from './machine-card';
import { MachinesFilters } from './machines-filters';
import { User } from '../../models/user';
declare var Application: IApplication;
declare const Application: IApplication;
interface MachinesListProps {
user?: User,
@ -39,7 +39,7 @@ const MachinesList: React.FC<MachinesListProps> = ({ onError, onSuccess, onShowM
// filter the machines shown when the full list was retrieved
useEffect(() => {
handleFilterByStatus(true);
}, [allMachines])
}, [allMachines]);
/**
* Callback triggered when the user changes the status filter.
@ -53,7 +53,7 @@ const MachinesList: React.FC<MachinesListProps> = ({ onError, onSuccess, onShowM
// enabled machines may have the m.disabled property null (for never disabled machines)
// or false (for re-enabled machines)
setMachines(allMachines.filter(m => !!m.disabled === !status));
}
};
return (
<div className="machines-list">
@ -61,20 +61,19 @@ const MachinesList: React.FC<MachinesListProps> = ({ onError, onSuccess, onShowM
<div className="all-machines">
{machines && machines.map(machine => {
return <MachineCard key={machine.id}
user={user}
machine={machine}
onShowMachine={onShowMachine}
onReserveMachine={onReserveMachine}
onError={onError}
onSuccess={onSuccess}
onLoginRequested={onLoginRequested}
onEnrollRequested={onEnrollRequested} />
user={user}
machine={machine}
onShowMachine={onShowMachine}
onReserveMachine={onReserveMachine}
onError={onError}
onSuccess={onSuccess}
onLoginRequested={onLoginRequested}
onEnrollRequested={onEnrollRequested} />;
})}
</div>
</div>
);
}
};
const MachinesListWrapper: React.FC<MachinesListProps> = ({ user, onError, onSuccess, onShowMachine, onReserveMachine, onLoginRequested, onEnrollRequested }) => {
return (
@ -82,6 +81,6 @@ const MachinesListWrapper: React.FC<MachinesListProps> = ({ user, onError, onSuc
<MachinesList user={user} onError={onError} onSuccess={onSuccess} onShowMachine={onShowMachine} onReserveMachine={onReserveMachine} onLoginRequested={onLoginRequested} onEnrollRequested={onEnrollRequested} />
</Loader>
);
}
};
Application.Components.component('machinesList', react2angular(MachinesListWrapper, ['user', 'onError', 'onSuccess', 'onShowMachine', 'onReserveMachine', 'onLoginRequested', 'onEnrollRequested']));

View File

@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
import { HtmlTranslate } from '../base/html-translate';
import { IFablab } from '../../models/fablab';
declare var Fablab: IFablab;
declare let Fablab: IFablab;
interface PendingTrainingModalProps {
isOpen: boolean,
@ -26,16 +26,16 @@ export const PendingTrainingModal: React.FC<PendingTrainingModalProps> = ({ isOp
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: day, TIME: time });
};
return (
<FabModal title={t('app.logged.pending_training_modal.machine_reservation')}
isOpen={isOpen}
toggleModal={toggleModal}
closeButton={true}>
isOpen={isOpen}
toggleModal={toggleModal}
closeButton={true}>
<p>{t('app.logged.pending_training_modal.wait_for_validated')}</p>
<p><HtmlTranslate trKey="app.logged.pending_training_modal.training_will_occur_DATE_html" options={{ DATE: formatDateTime(nextReservation) }} /></p>
</FabModal>
)
}
);
};

View File

@ -27,14 +27,14 @@ export const RequiredTrainingModal: React.FC<RequiredTrainingModalProps> = ({ is
if (!machine) return '';
return machine.trainings.map(t => t.name).join(t('app.logged.required_training_modal.training_or_training_html'));
}
};
/**
* Callback triggered when the user has clicked on the "enroll" button
*/
const handleEnroll = (): void => {
onEnrollRequested(machine.trainings[0].id);
}
};
/**
* Custom header of the dialog: we display the username and avatar
@ -46,7 +46,7 @@ export const RequiredTrainingModal: React.FC<RequiredTrainingModalProps> = ({ is
<span className="user-name">{user?.name}</span>
</div>
);
}
};
/**
* Custom footer of the dialog: we display a user-friendly message to close the dialog
@ -58,24 +58,24 @@ export const RequiredTrainingModal: React.FC<RequiredTrainingModalProps> = ({ is
<a onClick={toggleModal}>{t('app.logged.required_training_modal.close')}</a>
</div>
);
}
};
return (
<FabModal isOpen={isOpen}
toggleModal={toggleModal}
className="required-training-modal"
closeButton={false}
customHeader={header()}
customFooter={footer()}>
toggleModal={toggleModal}
className="required-training-modal"
closeButton={false}
customHeader={header()}
customFooter={footer()}>
<div className="training-info">
<p>
<HtmlTranslate trKey={'app.logged.required_training_modal.to_book_MACHINE_requires_TRAINING_html'}
options={{ MACHINE: machine?.name, TRAINING: formatTrainings() }} />
options={{ MACHINE: machine?.name, TRAINING: formatTrainings() }} />
</p>
<div className="enroll-container">
<FabButton onClick={handleEnroll}>{t('app.logged.required_training_modal.enroll_now')}</FabButton>
</div>
</div>
</FabModal>
)
}
);
};

View File

@ -10,7 +10,7 @@ import { Machine } from '../../models/machine';
import { User } from '../../models/user';
import { IApplication } from '../../models/application';
declare var Application: IApplication;
declare const Application: IApplication;
interface ReserveButtonProps {
currentUser?: User,
@ -86,7 +86,7 @@ const ReserveButtonComponent: React.FC<ReserveButtonProps> = ({ currentUser, mac
*/
const toggleProposePacksModal = (): void => {
setProposePacks(!proposePacks);
}
};
/**
* Callback triggered when the user has successfully bought a pre-paid pack.
@ -95,7 +95,7 @@ const ReserveButtonComponent: React.FC<ReserveButtonProps> = ({ currentUser, mac
const handlePackBought = (message: string, machine: Machine): void => {
onSuccess(message);
onReserveMachine(machine);
}
};
/**
* Check that the current user has passed the required training before allowing him to book
@ -143,35 +143,35 @@ const ReserveButtonComponent: React.FC<ReserveButtonProps> = ({ currentUser, mac
// otherwise, we show a dialog modal to propose the customer to buy an available pack
setProposePacks(true);
}
};
return (
<span>
<button onClick={handleClick} className={className ? className : ''}>
<button onClick={handleClick} className={className || ''}>
{children && children}
{!children && <span>{t('app.shared.reserve_button.book_this_machine')}</span>}
</button>
<PendingTrainingModal isOpen={pendingTraining}
toggleModal={togglePendingTrainingModal}
nextReservation={machine?.current_user_next_training_reservation?.slots_attributes[0]?.start_at} />
toggleModal={togglePendingTrainingModal}
nextReservation={machine?.current_user_next_training_reservation?.slots_attributes[0]?.start_at} />
<RequiredTrainingModal isOpen={trainingRequired}
toggleModal={toggleRequiredTrainingModal}
user={user}
machine={machine}
onEnrollRequested={onEnrollRequested} />
toggleModal={toggleRequiredTrainingModal}
user={user}
machine={machine}
onEnrollRequested={onEnrollRequested} />
{machine && currentUser && <ProposePacksModal isOpen={proposePacks}
toggleModal={toggleProposePacksModal}
item={machine}
itemType="Machine"
onError={onError}
customer={currentUser}
onDecline={onReserveMachine}
operator={currentUser}
onSuccess={handlePackBought} />}
toggleModal={toggleProposePacksModal}
item={machine}
itemType="Machine"
onError={onError}
customer={currentUser}
onDecline={onReserveMachine}
operator={currentUser}
onSuccess={handlePackBought} />}
</span>
);
}
};
export const ReserveButton: React.FC<ReserveButtonProps> = ({ currentUser, machineId, onLoginRequested, onLoadingStart, onLoadingEnd, onError, onSuccess, onReserveMachine, onEnrollRequested, className, children }) => {
return (
@ -181,6 +181,6 @@ export const ReserveButton: React.FC<ReserveButtonProps> = ({ currentUser, machi
</ReserveButtonComponent>
</Loader>
);
}
};
Application.Components.component('reserveButton', react2angular(ReserveButton, ['currentUser', 'machineId', 'onLoadingStart', 'onLoadingEnd', 'onError', 'onSuccess', 'onReserveMachine', 'onLoginRequested', 'onEnrollRequested', 'className']));

View File

@ -8,7 +8,7 @@ import { PaymentSchedule } from '../../models/payment-schedule';
import { IApplication } from '../../models/application';
import FormatLib from '../../lib/format';
declare var Application: IApplication;
declare const Application: IApplication;
interface PaymentScheduleSummaryProps {
schedule: PaymentSchedule
@ -29,13 +29,13 @@ const PaymentScheduleSummary: React.FC<PaymentScheduleSummaryProps> = ({ schedul
const hasEqualDeadlines = (): boolean => {
const prices = schedule.items.map(i => i.amount);
return prices.every(p => p === prices[0]);
}
};
/**
* Open or closes the modal dialog showing the full payment schedule
*/
const toggleFullScheduleModal = (): void => {
setModal(!modal);
}
};
return (
<div className="payment-schedule-summary">
@ -64,25 +64,25 @@ const PaymentScheduleSummary: React.FC<PaymentScheduleSummaryProps> = ({ schedul
<button className="view-full-schedule" onClick={toggleFullScheduleModal}>{t('app.shared.cart.view_full_schedule')}</button>
<FabModal title={t('app.shared.cart.your_payment_schedule')} isOpen={modal} toggleModal={toggleFullScheduleModal}>
<ul className="full-schedule">
{schedule.items.map(item => (
<li key={String(item.due_date)}>
<span className="schedule-item-date">{FormatLib.date(item.due_date)}</span>
<span> </span>
<span className="schedule-item-price">{FormatLib.price(item.amount)}</span>
</li>
))}
{schedule.items.map(item => (
<li key={String(item.due_date)}>
<span className="schedule-item-date">{FormatLib.date(item.due_date)}</span>
<span> </span>
<span className="schedule-item-price">{FormatLib.price(item.amount)}</span>
</li>
))}
</ul>
</FabModal>
</div>
</div>
);
}
};
const PaymentScheduleSummaryWrapper: React.FC<PaymentScheduleSummaryProps> = ({ schedule }) => {
return (
<Loader>
<PaymentScheduleSummary schedule={schedule} />
</Loader>
);
}
};
Application.Components.component('paymentScheduleSummary', react2angular(PaymentScheduleSummaryWrapper, ['schedule']));

View File

@ -9,7 +9,7 @@ import { PaymentSchedule } from '../../models/payment-schedule';
import { IApplication } from '../../models/application';
import PaymentScheduleAPI from '../../api/payment-schedule';
declare var Application: IApplication;
declare const Application: IApplication;
interface PaymentSchedulesDashboardProps {
currentUser: User,
@ -45,60 +45,59 @@ const PaymentSchedulesDashboard: React.FC<PaymentSchedulesDashboardProps> = ({ c
const handleLoadMore = (): void => {
setPageNumber(pageNumber + 1);
PaymentScheduleAPI.index({ query: { page: pageNumber + 1, size: PAGE_SIZE }}).then((res) => {
PaymentScheduleAPI.index({ query: { page: pageNumber + 1, size: PAGE_SIZE } }).then((res) => {
const list = paymentSchedules.concat(res);
setPaymentSchedules(list);
}).catch((error) => onError(error.message));
}
};
/**
* Reload from te API all the currently displayed payment schedules
*/
const handleRefreshList = (): void => {
PaymentScheduleAPI.index({ query: { page: 1, size: PAGE_SIZE * pageNumber }}).then((res) => {
PaymentScheduleAPI.index({ query: { page: 1, size: PAGE_SIZE * pageNumber } }).then((res) => {
setPaymentSchedules(res);
}).catch((err) => {
onError(err.message);
});
}
};
/**
* after a successful card update, provide a success message to the end-user
*/
const handleCardUpdateSuccess = (): void => {
onCardUpdateSuccess(t('app.logged.dashboard.payment_schedules.card_updated_success'));
}
};
/**
* Check if the current collection of payment schedules is empty or not.
*/
const hasSchedules = (): boolean => {
return paymentSchedules.length > 0;
}
};
/**
* Check if there are some results for the current filters that aren't currently shown.
*/
const hasMoreSchedules = (): boolean => {
return hasSchedules() && paymentSchedules.length < paymentSchedules[0].max_length;
}
};
return (
<div className="payment-schedules-dashboard">
{!hasSchedules() && <div>{t('app.logged.dashboard.payment_schedules.no_payment_schedules')}</div>}
{hasSchedules() && <div className="schedules-list">
<PaymentSchedulesTable paymentSchedules={paymentSchedules}
showCustomer={false}
refreshList={handleRefreshList}
operator={currentUser}
onError={onError}
onCardUpdateSuccess={handleCardUpdateSuccess} />
showCustomer={false}
refreshList={handleRefreshList}
operator={currentUser}
onError={onError}
onCardUpdateSuccess={handleCardUpdateSuccess} />
{hasMoreSchedules() && <FabButton className="load-more" onClick={handleLoadMore}>{t('app.logged.dashboard.payment_schedules.load_more')}</FabButton>}
</div>}
</div>
);
}
};
const PaymentSchedulesDashboardWrapper: React.FC<PaymentSchedulesDashboardProps> = ({ currentUser, onError, onCardUpdateSuccess }) => {
return (
@ -106,6 +105,6 @@ const PaymentSchedulesDashboardWrapper: React.FC<PaymentSchedulesDashboardProps>
<PaymentSchedulesDashboard currentUser={currentUser} onError={onError} onCardUpdateSuccess={onCardUpdateSuccess} />
</Loader>
);
}
};
Application.Components.component('paymentSchedulesDashboard', react2angular(PaymentSchedulesDashboardWrapper, ['currentUser', 'onError', 'onCardUpdateSuccess']));

View File

@ -10,7 +10,7 @@ import { PaymentSchedule } from '../../models/payment-schedule';
import { IApplication } from '../../models/application';
import PaymentScheduleAPI from '../../api/payment-schedule';
declare var Application: IApplication;
declare const Application: IApplication;
interface PaymentSchedulesListProps {
currentUser: User,
@ -53,7 +53,7 @@ const PaymentSchedulesList: React.FC<PaymentSchedulesListProps> = ({ currentUser
setCustomerFilter(customer);
setDateFilter(date);
PaymentScheduleAPI.list({ query: { reference, customer, date, page: 1, size: PAGE_SIZE }}).then((res) => {
PaymentScheduleAPI.list({ query: { reference, customer, date, page: 1, size: PAGE_SIZE } }).then((res) => {
setPaymentSchedules(res);
}).catch((error) => onError(error.message));
};
@ -64,43 +64,43 @@ const PaymentSchedulesList: React.FC<PaymentSchedulesListProps> = ({ currentUser
const handleLoadMore = (): void => {
setPageNumber(pageNumber + 1);
PaymentScheduleAPI.list({ query: { reference: referenceFilter, customer: customerFilter, date: dateFilter, page: pageNumber + 1, size: PAGE_SIZE }}).then((res) => {
PaymentScheduleAPI.list({ query: { reference: referenceFilter, customer: customerFilter, date: dateFilter, page: pageNumber + 1, size: PAGE_SIZE } }).then((res) => {
const list = paymentSchedules.concat(res);
setPaymentSchedules(list);
}).catch((error) => onError(error.message));
}
};
/**
* Reload from te API all the currently displayed payment schedules
*/
const handleRefreshList = (): void => {
PaymentScheduleAPI.list({ query: { reference: referenceFilter, customer: customerFilter, date: dateFilter, page: 1, size: PAGE_SIZE * pageNumber }}).then((res) => {
PaymentScheduleAPI.list({ query: { reference: referenceFilter, customer: customerFilter, date: dateFilter, page: 1, size: PAGE_SIZE * pageNumber } }).then((res) => {
setPaymentSchedules(res);
}).catch((err) => {
onError(err.message);
});
}
};
/**
* Check if the current collection of payment schedules is empty or not.
*/
const hasSchedules = (): boolean => {
return paymentSchedules.length > 0;
}
};
/**
* Check if there are some results for the current filters that aren't currently shown.
*/
const hasMoreSchedules = (): boolean => {
return hasSchedules() && paymentSchedules.length < paymentSchedules[0].max_length;
}
};
/**
* after a successful card update, provide a success message to the operator
*/
const handleCardUpdateSuccess = (): void => {
onCardUpdateSuccess(t('app.admin.invoices.payment_schedules.card_updated_success'));
}
};
return (
<div className="payment-schedules-list">
@ -114,17 +114,16 @@ const PaymentSchedulesList: React.FC<PaymentSchedulesListProps> = ({ currentUser
{!hasSchedules() && <div>{t('app.admin.invoices.payment_schedules.no_payment_schedules')}</div>}
{hasSchedules() && <div className="schedules-list">
<PaymentSchedulesTable paymentSchedules={paymentSchedules}
showCustomer={true}
refreshList={handleRefreshList}
operator={currentUser}
onError={onError}
onCardUpdateSuccess={handleCardUpdateSuccess} />
showCustomer={true}
refreshList={handleRefreshList}
operator={currentUser}
onError={onError}
onCardUpdateSuccess={handleCardUpdateSuccess} />
{hasMoreSchedules() && <FabButton className="load-more" onClick={handleLoadMore}>{t('app.admin.invoices.payment_schedules.load_more')}</FabButton>}
</div>}
</div>
);
}
};
const PaymentSchedulesListWrapper: React.FC<PaymentSchedulesListProps> = ({ currentUser, onError, onCardUpdateSuccess }) => {
return (
@ -132,6 +131,6 @@ const PaymentSchedulesListWrapper: React.FC<PaymentSchedulesListProps> = ({ curr
<PaymentSchedulesList currentUser={currentUser} onError={onError} onCardUpdateSuccess={onCardUpdateSuccess} />
</Loader>
);
}
};
Application.Components.component('paymentSchedulesList', react2angular(PaymentSchedulesListWrapper, ['currentUser', 'onError', 'onCardUpdateSuccess']));

View File

@ -52,18 +52,18 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
*/
const isExpanded = (paymentScheduleId: number): boolean => {
return showExpanded.get(paymentScheduleId);
}
};
/**
* Return the value for the CSS property 'display', for the payment schedule deadlines
*/
const statusDisplay = (paymentScheduleId: number): string => {
if (isExpanded(paymentScheduleId)) {
return 'table-row'
return 'table-row';
} else {
return 'none';
}
}
};
/**
* Return the action icon for showing/hiding the deadlines
@ -72,9 +72,9 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
if (isExpanded(paymentScheduleId)) {
return <i className="fas fa-minus-square" />;
} else {
return <i className="fas fa-plus-square" />
return <i className="fas fa-plus-square" />;
}
}
};
/**
* Show or hide the deadlines for the provided payment schedule, inverting their current status
@ -86,8 +86,8 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
} else {
setShowExpanded((prev) => new Map(prev).set(paymentScheduleId, true));
}
}
}
};
};
/**
* For use with downloadButton()
@ -103,12 +103,12 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
const downloadButton = (target: TargetType, id: number): JSX.Element => {
const link = `api/${target}/${id}/download`;
return (
<a href={link} target="_blank" className="download-button">
<a href={link} target="_blank" className="download-button" rel="noreferrer">
<i className="fas fa-download" />
{t('app.shared.schedules_table.download')}
</a>
);
}
};
/**
* Return the human-readable string for the status of the provided deadline.
@ -116,18 +116,18 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
const formatState = (item: PaymentScheduleItem): JSX.Element => {
let res = t(`app.shared.schedules_table.state_${item.state}`);
if (item.state === PaymentScheduleItemState.Paid) {
const key = `app.shared.schedules_table.method_${item.payment_method}`
const key = `app.shared.schedules_table.method_${item.payment_method}`;
res += ` (${t(key)})`;
}
return <span className={`state-${item.state}`}>{res}</span>;
}
};
/**
* Check if the current operator has administrative rights or is a normal member
*/
const isPrivileged = (): boolean => {
return (operator.role === UserRole.Admin || operator.role == UserRole.Manager);
}
return (operator.role === UserRole.Admin || operator.role === UserRole.Manager);
};
/**
* Return the action button(s) for the given deadline
@ -140,24 +140,24 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
if (isPrivileged()) {
return (
<FabButton onClick={handleConfirmCheckPayment(item)}
icon={<i className="fas fa-money-check" />}>
icon={<i className="fas fa-money-check" />}>
{t('app.shared.schedules_table.confirm_payment')}
</FabButton>
);
} else {
return <span>{t('app.shared.schedules_table.please_ask_reception')}</span>
return <span>{t('app.shared.schedules_table.please_ask_reception')}</span>;
}
case PaymentScheduleItemState.RequireAction:
return (
<FabButton onClick={handleSolveAction(item)}
icon={<i className="fas fa-wrench" />}>
icon={<i className="fas fa-wrench" />}>
{t('app.shared.schedules_table.solve')}
</FabButton>
);
case PaymentScheduleItemState.RequirePaymentMethod:
return (
<FabButton onClick={handleUpdateCard(schedule, item)}
icon={<i className="fas fa-credit-card" />}>
icon={<i className="fas fa-credit-card" />}>
{t('app.shared.schedules_table.update_card')}
</FabButton>
);
@ -167,28 +167,28 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
if (isPrivileged()) {
return (
<FabButton onClick={handleCancelSubscription(schedule)}
icon={<i className="fas fa-times" />}>
icon={<i className="fas fa-times" />}>
{t('app.shared.schedules_table.cancel_subscription')}
</FabButton>
)
);
} else {
return <span>{t('app.shared.schedules_table.please_ask_reception')}</span>
return <span>{t('app.shared.schedules_table.please_ask_reception')}</span>;
}
case PaymentScheduleItemState.New:
if (!cardUpdateButton.get(schedule.id)) {
cardUpdateButton.set(schedule.id, true);
return (
<FabButton onClick={handleUpdateCard(schedule)}
icon={<i className="fas fa-credit-card" />}>
icon={<i className="fas fa-credit-card" />}>
{t('app.shared.schedules_table.update_card')}
</FabButton>
)
);
}
return <span />
return <span />;
default:
return <span />
return <span />;
}
}
};
/**
* Callback triggered when the user's clicks on the "cash check" button: show a confirmation modal
@ -197,8 +197,8 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
return (): void => {
setTempDeadline(item);
toggleConfirmCashingModal();
}
}
};
};
/**
* After the user has confirmed that he wants to cash the check, update the API, refresh the list and close the modal.
@ -210,28 +210,28 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
toggleConfirmCashingModal();
}
});
}
};
/**
* Refresh all payment schedules in the table
*/
const refreshSchedulesTable = (): void => {
refreshList();
}
};
/**
* Show/hide the modal dialog that enable to confirm the cashing of the check for a given deadline.
*/
const toggleConfirmCashingModal = (): void => {
setShowConfirmCashing(!showConfirmCashing);
}
};
/**
* Show/hide the modal dialog that trigger the card "action".
*/
const toggleResolveActionModal = (): void => {
setShowResolveAction(!showResolveAction);
}
};
/**
* Callback triggered when the user's clicks on the "resolve" button: show a modal that will trigger the action
@ -240,8 +240,8 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
return (): void => {
setTempDeadline(item);
toggleResolveActionModal();
}
}
};
};
/**
* After the action was done (successfully or not), ask the API to refresh the item status, then refresh the list and close the modal
@ -252,14 +252,14 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
refreshSchedulesTable();
toggleResolveActionModal();
});
}
};
/**
* Enable/disable the confirm button of the "action" modal
*/
const toggleConfirmActionButton = (): void => {
setConfirmActionDisabled(!isConfirmActionDisabled);
}
};
/**
* Callback triggered when the user's clicks on the "update card" button: show a modal to input a new card
@ -269,15 +269,15 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
setTempDeadline(item);
setTempSchedule(paymentSchedule);
toggleUpdateCardModal();
}
}
};
};
/**
* Show/hide the modal dialog to update the bank card details
*/
const toggleUpdateCardModal = (): void => {
setShowUpdateCard(!showUpdateCard);
}
};
/**
* When the card was successfully updated, pay the invoice (using the new payment method) and close the modal
@ -296,14 +296,14 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
onCardUpdateSuccess();
toggleUpdateCardModal();
}
}
};
/**
* When the card was not updated, raise the error
*/
const handleCardUpdateError = (error): void => {
onError(error);
}
};
/**
* Callback triggered when the user clicks on the "cancel subscription" button
@ -312,15 +312,15 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
return (): void => {
setTempSchedule(schedule);
toggleCancelSubscriptionModal();
}
}
};
};
/**
* Show/hide the modal dialog to cancel the current subscription
*/
const toggleCancelSubscriptionModal = (): void => {
setShowCancelSubscription(!showCancelSubscription);
}
};
/**
* When the user has confirmed the cancellation, we transfer the request to the API
@ -330,72 +330,72 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
refreshSchedulesTable();
toggleCancelSubscriptionModal();
});
}
};
return (
<div>
<table className="schedules-table">
<thead>
<tr>
<th className="w-35" />
<th className="w-200">{t('app.shared.schedules_table.schedule_num')}</th>
<th className="w-200">{t('app.shared.schedules_table.date')}</th>
<th className="w-120">{t('app.shared.schedules_table.price')}</th>
{showCustomer && <th className="w-200">{t('app.shared.schedules_table.customer')}</th>}
<th className="w-200"/>
</tr>
<tr>
<th className="w-35" />
<th className="w-200">{t('app.shared.schedules_table.schedule_num')}</th>
<th className="w-200">{t('app.shared.schedules_table.date')}</th>
<th className="w-120">{t('app.shared.schedules_table.price')}</th>
{showCustomer && <th className="w-200">{t('app.shared.schedules_table.customer')}</th>}
<th className="w-200"/>
</tr>
</thead>
<tbody>
{paymentSchedules.map(p => <tr key={p.id}>
<td colSpan={showCustomer ? 6 : 5}>
<table className="schedules-table-body">
<tbody>
<tr>
<td className="w-35 row-header" onClick={togglePaymentScheduleDetails(p.id)}>{expandCollapseIcon(p.id)}</td>
<td className="w-200">{p.reference}</td>
<td className="w-200">{FormatLib.date(p.created_at)}</td>
<td className="w-120">{FormatLib.price(p.total)}</td>
{showCustomer && <td className="w-200">{p.user.name}</td>}
<td className="w-200">{downloadButton(TargetType.PaymentSchedule, p.id)}</td>
</tr>
<tr style={{ display: statusDisplay(p.id) }}>
<td className="w-35" />
<td colSpan={showCustomer ? 5 : 4}>
<div>
<table className="schedule-items-table">
<thead>
<tr>
<th className="w-120">{t('app.shared.schedules_table.deadline')}</th>
<th className="w-120">{t('app.shared.schedules_table.amount')}</th>
<th className="w-200">{t('app.shared.schedules_table.state')}</th>
<th className="w-200" />
</tr>
</thead>
<tbody>
{_.orderBy(p.items, 'due_date').map(item => <tr key={item.id}>
<td>{FormatLib.date(item.due_date)}</td>
<td>{FormatLib.price(item.amount)}</td>
<td>{formatState(item)}</td>
<td>{itemButtons(item, p)}</td>
</tr>)}
</tbody>
</table>
</div>
</td>
</tr>
</tbody>
</table>
</td>
</tr>)}
{paymentSchedules.map(p => <tr key={p.id}>
<td colSpan={showCustomer ? 6 : 5}>
<table className="schedules-table-body">
<tbody>
<tr>
<td className="w-35 row-header" onClick={togglePaymentScheduleDetails(p.id)}>{expandCollapseIcon(p.id)}</td>
<td className="w-200">{p.reference}</td>
<td className="w-200">{FormatLib.date(p.created_at)}</td>
<td className="w-120">{FormatLib.price(p.total)}</td>
{showCustomer && <td className="w-200">{p.user.name}</td>}
<td className="w-200">{downloadButton(TargetType.PaymentSchedule, p.id)}</td>
</tr>
<tr style={{ display: statusDisplay(p.id) }}>
<td className="w-35" />
<td colSpan={showCustomer ? 5 : 4}>
<div>
<table className="schedule-items-table">
<thead>
<tr>
<th className="w-120">{t('app.shared.schedules_table.deadline')}</th>
<th className="w-120">{t('app.shared.schedules_table.amount')}</th>
<th className="w-200">{t('app.shared.schedules_table.state')}</th>
<th className="w-200" />
</tr>
</thead>
<tbody>
{_.orderBy(p.items, 'due_date').map(item => <tr key={item.id}>
<td>{FormatLib.date(item.due_date)}</td>
<td>{FormatLib.price(item.amount)}</td>
<td>{formatState(item)}</td>
<td>{itemButtons(item, p)}</td>
</tr>)}
</tbody>
</table>
</div>
</td>
</tr>
</tbody>
</table>
</td>
</tr>)}
</tbody>
</table>
<div className="modals">
<FabModal title={t('app.shared.schedules_table.confirm_check_cashing')}
isOpen={showConfirmCashing}
toggleModal={toggleConfirmCashingModal}
onConfirm={onCheckCashingConfirmed}
closeButton={true}
confirmButton={t('app.shared.schedules_table.confirm_button')}>
isOpen={showConfirmCashing}
toggleModal={toggleConfirmCashingModal}
onConfirm={onCheckCashingConfirmed}
closeButton={true}
confirmButton={t('app.shared.schedules_table.confirm_button')}>
{tempDeadline && <span>
{t('app.shared.schedules_table.confirm_check_cashing_body', {
AMOUNT: FormatLib.price(tempDeadline.amount),
@ -404,28 +404,28 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
</span>}
</FabModal>
<FabModal title={t('app.shared.schedules_table.cancel_subscription')}
isOpen={showCancelSubscription}
toggleModal={toggleCancelSubscriptionModal}
onConfirm={onCancelSubscriptionConfirmed}
closeButton={true}
confirmButton={t('app.shared.schedules_table.confirm_button')}>
isOpen={showCancelSubscription}
toggleModal={toggleCancelSubscriptionModal}
onConfirm={onCancelSubscriptionConfirmed}
closeButton={true}
confirmButton={t('app.shared.schedules_table.confirm_button')}>
{t('app.shared.schedules_table.confirm_cancel_subscription')}
</FabModal>
<StripeElements>
<FabModal title={t('app.shared.schedules_table.resolve_action')}
isOpen={showResolveAction}
toggleModal={toggleResolveActionModal}
onConfirm={afterAction}
confirmButton={t('app.shared.schedules_table.ok_button')}
preventConfirm={isConfirmActionDisabled}>
isOpen={showResolveAction}
toggleModal={toggleResolveActionModal}
onConfirm={afterAction}
confirmButton={t('app.shared.schedules_table.ok_button')}
preventConfirm={isConfirmActionDisabled}>
{tempDeadline && <StripeConfirm clientSecret={tempDeadline.client_secret} onResponse={toggleConfirmActionButton} />}
</FabModal>
{tempSchedule && <UpdateCardModal isOpen={showUpdateCard}
toggleModal={toggleUpdateCardModal}
operator={operator}
afterSuccess={handleCardUpdateSuccess}
onError={handleCardUpdateError}
schedule={tempSchedule}>
toggleModal={toggleUpdateCardModal}
operator={operator}
afterSuccess={handleCardUpdateSuccess}
onError={handleCardUpdateError}
schedule={tempSchedule}>
</UpdateCardModal>}
</StripeElements>
</div>
@ -434,11 +434,10 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
};
PaymentSchedulesTableComponent.defaultProps = { showCustomer: false };
export const PaymentSchedulesTable: React.FC<PaymentSchedulesTableProps> = ({ paymentSchedules, showCustomer, refreshList, operator, onError, onCardUpdateSuccess }) => {
return (
<Loader>
<PaymentSchedulesTableComponent paymentSchedules={paymentSchedules} showCustomer={showCustomer} refreshList={refreshList} operator={operator} onError={onError} onCardUpdateSuccess={onCardUpdateSuccess} />
</Loader>
);
}
};

View File

@ -1,4 +1,4 @@
import React from 'react';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { react2angular } from 'react2angular';
import Switch from 'react-switch';
@ -6,7 +6,7 @@ import '../../lib/i18n';
import { Loader } from '../base/loader';
import { IApplication } from '../../models/application';
declare var Application: IApplication;
declare const Application: IApplication;
interface SelectScheduleProps {
show: boolean,
@ -30,7 +30,7 @@ const SelectSchedule: React.FC<SelectScheduleProps> = ({ show, selected, onChang
</div>}
</div>
);
}
};
const SelectScheduleWrapper: React.FC<SelectScheduleProps> = ({ show, selected, onChange, className }) => {
return (
@ -38,6 +38,6 @@ const SelectScheduleWrapper: React.FC<SelectScheduleProps> = ({ show, selected,
<SelectSchedule show={show} selected={selected} onChange={onChange} className={className} />
</Loader>
);
}
};
Application.Components.component('selectSchedule', react2angular(SelectScheduleWrapper, ['show', 'selected', 'onChange', 'className']));

View File

@ -18,7 +18,6 @@ import { ComputePriceResult } from '../../models/price';
import { Wallet } from '../../models/wallet';
import FormatLib from '../../lib/format';
export interface GatewayFormProps {
onSubmit: () => void,
onSuccess: (result: Invoice|PaymentSchedule) => void,
@ -78,7 +77,6 @@ export const AbstractPaymentModal: React.FC<AbstractPaymentModalProps> = ({ isOp
const { t } = useTranslation('shared');
/**
* When the component loads first, get the name of the currently active payment modal
*/
@ -87,7 +85,7 @@ export const AbstractPaymentModal: React.FC<AbstractPaymentModalProps> = ({ isOp
SettingAPI.get(SettingName.PaymentGateway).then((setting) => {
// we capitalize the first letter of the name
setGateway(setting.value.replace(/^\w/, (c) => c.toUpperCase()));
})
});
}, []);
/**
@ -104,8 +102,8 @@ export const AbstractPaymentModal: React.FC<AbstractPaymentModalProps> = ({ isOp
setPrice(res);
setRemainingPrice(new WalletLib(wallet).computeRemainingPrice(res.price));
setReady(true);
})
})
});
});
}, [cart]);
/**
@ -113,35 +111,35 @@ export const AbstractPaymentModal: React.FC<AbstractPaymentModalProps> = ({ isOp
*/
const hasErrors = (): boolean => {
return errors !== null;
}
};
/**
* Check if the user accepts the Terms of Sales document
*/
const hasCgv = (): boolean => {
return cgv != null && !preventCgv;
}
};
/**
* Triggered when the user accepts or declines the Terms of Sales
*/
const toggleTos = (): void => {
setTos(!tos);
}
};
/**
* Check if we must display the info box about the payment schedule
*/
const hasPaymentScheduleInfo = (): boolean => {
return schedule !== undefined && !preventScheduleInfo;
}
};
/**
* Set the component as 'currently submitting'
*/
const handleSubmit = (): void => {
setSubmitState(true);
}
};
/**
* After sending the form with success, process the resulting payment method
@ -149,7 +147,7 @@ export const AbstractPaymentModal: React.FC<AbstractPaymentModalProps> = ({ isOp
const handleFormSuccess = async (result: Invoice|PaymentSchedule): Promise<void> => {
setSubmitState(false);
afterSuccess(result);
}
};
/**
* When the payment form raises an error, it is handled by this callback which display it in the modal.
@ -157,7 +155,7 @@ export const AbstractPaymentModal: React.FC<AbstractPaymentModalProps> = ({ isOp
const handleFormError = (message: string): void => {
setSubmitState(false);
setErrors(message);
}
};
/**
* Check the form can be submitted.
@ -167,7 +165,7 @@ export const AbstractPaymentModal: React.FC<AbstractPaymentModalProps> = ({ isOp
let terms = true;
if (hasCgv()) { terms = tos; }
return !submitState && terms;
}
};
/**
* Build the modal title. If the provided title is a shared translation key, interpolate it through the
@ -178,28 +176,27 @@ export const AbstractPaymentModal: React.FC<AbstractPaymentModalProps> = ({ isOp
return t(title);
}
return title;
}
};
return (
<FabModal title={getTitle()}
isOpen={isOpen}
toggleModal={toggleModal}
width={modalSize}
closeButton={false}
customFooter={logoFooter}
className={`payment-modal ${className ? className : ''}`}>
isOpen={isOpen}
toggleModal={toggleModal}
width={modalSize}
closeButton={false}
customFooter={logoFooter}
className={`payment-modal ${className || ''}`}>
{ready && <div>
<WalletInfo cart={cart} currentUser={currentUser} wallet={wallet} price={price?.price} />
<GatewayForm onSubmit={handleSubmit}
onSuccess={handleFormSuccess}
onError={handleFormError}
operator={currentUser}
className={`gateway-form ${formClassName ? formClassName : ''}`}
formId={formId}
cart={cart}
customer={customer}
paymentSchedule={schedule}>
onSuccess={handleFormSuccess}
onError={handleFormError}
operator={currentUser}
className={`gateway-form ${formClassName || ''}`}
formId={formId}
cart={cart}
customer={customer}
paymentSchedule={schedule}>
{hasErrors() && <div className="payment-errors">
{errors}
</div>}
@ -209,16 +206,16 @@ export const AbstractPaymentModal: React.FC<AbstractPaymentModalProps> = ({ isOp
{hasCgv() && <div className="terms-of-sales">
<input type="checkbox" id="acceptToS" name="acceptCondition" checked={tos} onChange={toggleTos} required />
<label htmlFor="acceptToS">{ t('app.shared.payment.i_have_read_and_accept_') }
<a href={cgv.custom_asset_file_attributes.attachment_url} target="_blank">
<a href={cgv.custom_asset_file_attributes.attachment_url} target="_blank" rel="noreferrer">
{ t('app.shared.payment._the_general_terms_and_conditions') }
</a>
</label>
</div>}
</GatewayForm>
{!submitState && <button type="submit"
disabled={!canSubmit()}
form={formId}
className="validate-btn">
disabled={!canSubmit()}
form={formId}
className="validate-btn">
{t('app.shared.payment.confirm_payment_of_', { AMOUNT: FormatLib.price(remainingPrice) })}
</button>}
{submitState && <div className="payment-pending">
@ -229,7 +226,7 @@ export const AbstractPaymentModal: React.FC<AbstractPaymentModalProps> = ({ isOp
</div>}
</FabModal>
);
}
};
AbstractPaymentModal.defaultProps = {
title: 'app.shared.payment.online_payment',

View File

@ -9,7 +9,6 @@ import { SettingName } from '../../../models/setting';
import { PaymentModal } from '../payment-modal';
import { PaymentSchedule } from '../../../models/payment-schedule';
const ALL_SCHEDULE_METHODS = ['card', 'check'] as const;
type scheduleMethod = typeof ALL_SCHEDULE_METHODS[number];
@ -25,7 +24,6 @@ type selectOption = { value: scheduleMethod, label: string };
* The form validation button must be created elsewhere, using the attribute form={formId}.
*/
export const LocalPaymentForm: React.FC<GatewayFormProps> = ({ onSubmit, onSuccess, onError, children, className, paymentSchedule, cart, customer, operator, formId }) => {
const { t } = useTranslation('admin');
const [method, setMethod] = useState<scheduleMethod>('check');
@ -36,14 +34,14 @@ export const LocalPaymentForm: React.FC<GatewayFormProps> = ({ onSubmit, onSucce
*/
const toggleOnlinePaymentModal = (): void => {
setOnlinePaymentModal(!onlinePaymentModal);
}
};
/**
* Convert all payement methods for schedules to the react-select format
*/
const buildMethodOptions = (): Array<selectOption> => {
return ALL_SCHEDULE_METHODS.map(i => methodToOption(i));
}
};
/**
* Convert the given payment-method to the react-select format
@ -52,15 +50,14 @@ export const LocalPaymentForm: React.FC<GatewayFormProps> = ({ onSubmit, onSucce
if (!value) return { value, label: '' };
return { value, label: t(`app.admin.local_payment.method_${value}`) };
}
};
/**
* Callback triggered when the user selects a payment method for the current payment schedule.
*/
const handleUpdateMethod = (option: selectOption) => {
setMethod(option.value);
}
};
/**
* Handle the submission of the form. It will process the local payment.
@ -74,7 +71,7 @@ export const LocalPaymentForm: React.FC<GatewayFormProps> = ({ onSubmit, onSucce
try {
const online = await SettingAPI.get(SettingName.OnlinePaymentModule);
if (online.value !== 'true') {
return onError(t('app.admin.local_payment.online_payment_disabled'))
return onError(t('app.admin.local_payment.online_payment_disabled'));
}
return toggleOnlinePaymentModal();
} catch (e) {
@ -88,7 +85,7 @@ export const LocalPaymentForm: React.FC<GatewayFormProps> = ({ onSubmit, onSucce
} catch (e) {
onError(e);
}
}
};
/**
* Callback triggered after a successful payment by online card for a schedule.
@ -96,20 +93,20 @@ export const LocalPaymentForm: React.FC<GatewayFormProps> = ({ onSubmit, onSucce
const afterCreatePaymentSchedule = (document: PaymentSchedule) => {
toggleOnlinePaymentModal();
onSuccess(document);
}
};
return (
<form onSubmit={handleSubmit} id={formId} className={className ? className : ''}>
<form onSubmit={handleSubmit} id={formId} className={className || ''}>
{!paymentSchedule && <p className="payment">{t('app.admin.local_payment.about_to_cash')}</p>}
{paymentSchedule && <div className="payment-schedule">
<div className="schedule-method">
<label htmlFor="payment-method">{t('app.admin.local_payment.payment_method')}</label>
<Select placeholder={ t('app.admin.local_payment.payment_method') }
id="payment-method"
className="method-select"
onChange={handleUpdateMethod}
options={buildMethodOptions()}
defaultValue={methodToOption(method)} />
id="payment-method"
className="method-select"
onChange={handleUpdateMethod}
options={buildMethodOptions()}
defaultValue={methodToOption(method)} />
{method === 'card' && <p>{t('app.admin.local_payment.card_collection_info')}</p>}
{method === 'check' && <p>{t('app.admin.local_payment.check_collection_info', { DEADLINES: paymentSchedule.items.length })}</p>}
</div>
@ -122,19 +119,19 @@ export const LocalPaymentForm: React.FC<GatewayFormProps> = ({ onSubmit, onSucce
<span> </span>
<span className="schedule-item-price">{FormatLib.price(item.amount)}</span>
</li>
)
);
})}
</ul>
</div>
<PaymentModal isOpen={onlinePaymentModal}
toggleModal={toggleOnlinePaymentModal}
afterSuccess={afterCreatePaymentSchedule}
onError={onError}
cart={cart}
currentUser={operator}
customer={customer} />
toggleModal={toggleOnlinePaymentModal}
afterSuccess={afterCreatePaymentSchedule}
onError={onError}
cart={cart}
currentUser={operator}
customer={customer} />
</div>}
{children}
</form>
);
}
};

View File

@ -11,7 +11,7 @@ import { Loader } from '../../base/loader';
import { react2angular } from 'react2angular';
import { IApplication } from '../../../models/application';
declare var Application: IApplication;
declare const Application: IApplication;
interface LocalPaymentModalProps {
isOpen: boolean,
@ -27,7 +27,6 @@ interface LocalPaymentModalProps {
* This component enables a privileged user to confirm a local payments.
*/
const LocalPaymentModalComponent: React.FC<LocalPaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, cart, currentUser, schedule, customer }) => {
const { t } = useTranslation('admin');
/**
@ -39,53 +38,53 @@ const LocalPaymentModalComponent: React.FC<LocalPaymentModalProps> = ({ isOpen,
<i className="fas fa-lock fa-2x" />
</div>
);
}
};
/**
* Integrates the LocalPaymentForm into the parent AbstractPaymentModal
*/
const renderForm: FunctionComponent<GatewayFormProps> = ({ onSubmit, onSuccess, onError, operator, className, formId, cart, customer, paymentSchedule, children}) => {
const renderForm: FunctionComponent<GatewayFormProps> = ({ onSubmit, onSuccess, onError, operator, className, formId, cart, customer, paymentSchedule, children }) => {
return (
<LocalPaymentForm onSubmit={onSubmit}
onSuccess={onSuccess}
onError={onError}
operator={operator}
className={className}
formId={formId}
cart={cart}
customer={customer}
paymentSchedule={paymentSchedule}>
onSuccess={onSuccess}
onError={onError}
operator={operator}
className={className}
formId={formId}
cart={cart}
customer={customer}
paymentSchedule={paymentSchedule}>
{children}
</LocalPaymentForm>
);
}
};
return (
<AbstractPaymentModal className="local-payment-modal"
isOpen={isOpen}
toggleModal={toggleModal}
logoFooter={logoFooter()}
title={t('app.admin.local_payment.offline_payment')}
formId="local-payment-form"
formClassName="local-payment-form"
currentUser={currentUser}
cart={cart}
customer={customer}
afterSuccess={afterSuccess}
schedule={schedule}
GatewayForm={renderForm}
modalSize={schedule ? ModalSize.large : ModalSize.medium}
preventCgv
preventScheduleInfo />
isOpen={isOpen}
toggleModal={toggleModal}
logoFooter={logoFooter()}
title={t('app.admin.local_payment.offline_payment')}
formId="local-payment-form"
formClassName="local-payment-form"
currentUser={currentUser}
cart={cart}
customer={customer}
afterSuccess={afterSuccess}
schedule={schedule}
GatewayForm={renderForm}
modalSize={schedule ? ModalSize.large : ModalSize.medium}
preventCgv
preventScheduleInfo />
);
}
};
export const LocalPaymentModal: React.FC<LocalPaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, currentUser, schedule , cart, customer }) => {
export const LocalPaymentModal: React.FC<LocalPaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, currentUser, schedule, cart, customer }) => {
return (
<Loader>
<LocalPaymentModalComponent isOpen={isOpen} toggleModal={toggleModal} afterSuccess={afterSuccess} currentUser={currentUser} schedule={schedule} cart={cart} customer={customer} />
</Loader>
);
}
};
Application.Components.component('localPaymentModal', react2angular(LocalPaymentModal, ['isOpen', 'toggleModal', 'afterSuccess', 'currentUser', 'schedule', 'cart', 'customer']));

View File

@ -12,7 +12,7 @@ import { Invoice } from '../../models/invoice';
import SettingAPI from '../../api/setting';
import { useTranslation } from 'react-i18next';
declare var Application: IApplication;
declare const Application: IApplication;
interface PaymentModalProps {
isOpen: boolean,
@ -29,7 +29,7 @@ interface PaymentModalProps {
* This component open a modal dialog for the configured payment gateway, allowing the user to input his card data
* to process an online payment.
*/
const PaymentModalComponent: React.FC<PaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, currentUser, schedule , cart, customer }) => {
const PaymentModalComponent: React.FC<PaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, currentUser, schedule, cart, customer }) => {
const { t } = useTranslation('shared');
const [gateway, setGateway] = useState<Setting>(null);
@ -45,26 +45,26 @@ const PaymentModalComponent: React.FC<PaymentModalProps> = ({ isOpen, toggleModa
*/
const renderStripeModal = (): ReactElement => {
return <StripeModal isOpen={isOpen}
toggleModal={toggleModal}
afterSuccess={afterSuccess}
cart={cart}
currentUser={currentUser}
schedule={schedule}
customer={customer} />
}
toggleModal={toggleModal}
afterSuccess={afterSuccess}
cart={cart}
currentUser={currentUser}
schedule={schedule}
customer={customer} />;
};
/**
* Render the PayZen payment modal
*/
const renderPayZenModal = (): ReactElement => {
return <PayZenModal isOpen={isOpen}
toggleModal={toggleModal}
afterSuccess={afterSuccess}
cart={cart}
currentUser={currentUser}
schedule={schedule}
customer={customer} />
}
toggleModal={toggleModal}
afterSuccess={afterSuccess}
cart={cart}
currentUser={currentUser}
schedule={schedule}
customer={customer} />;
};
/**
* Determine which gateway is enabled and return the appropriate payment modal
@ -85,15 +85,14 @@ const PaymentModalComponent: React.FC<PaymentModalProps> = ({ isOpen, toggleModa
console.error(`[PaymentModal] Unimplemented gateway: ${gateway.value}`);
return <div />;
}
}
};
export const PaymentModal: React.FC<PaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, currentUser, schedule , cart, customer }) => {
export const PaymentModal: React.FC<PaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, currentUser, schedule, cart, customer }) => {
return (
<Loader>
<PaymentModalComponent isOpen={isOpen} toggleModal={toggleModal} afterSuccess={afterSuccess} onError={onError} currentUser={currentUser} schedule={schedule} cart={cart} customer={customer} />
</Loader>
);
}
};
Application.Components.component('paymentModal', react2angular(PaymentModal, ['isOpen', 'toggleModal', 'afterSuccess', 'onError', 'currentUser', 'schedule', 'cart', 'customer']));

View File

@ -25,7 +25,7 @@ export const PayzenCardUpdateModal: React.FC<PayzenCardUpdateModalProps> = ({ is
const [errors, setErrors] = useState<string>(null);
// the unique identifier of the html form
const formId = "payzen-card";
const formId = 'payzen-card';
/**
* Return the logos, shown in the modal footer.
@ -38,15 +38,14 @@ export const PayzenCardUpdateModal: React.FC<PayzenCardUpdateModalProps> = ({ is
<img src={visaLogo} alt="visa" />
</div>
);
}
};
/**
* When the user clicks the submit button, we disable it to prevent double form submission
*/
const handleCardUpdateSubmit = (): void => {
setCanSubmitUpdateCard(false);
}
};
/**
* When the card was not updated, show the error
@ -54,24 +53,24 @@ export const PayzenCardUpdateModal: React.FC<PayzenCardUpdateModalProps> = ({ is
const handleCardUpdateError = (error): void => {
setErrors(error);
setCanSubmitUpdateCard(true);
}
};
return (
<FabModal title={t('app.shared.payzen_card_update_modal.update_card')}
isOpen={isOpen}
toggleModal={toggleModal}
closeButton={false}
customFooter={logoFooter()}
className="payzen-update-card-modal">
isOpen={isOpen}
toggleModal={toggleModal}
closeButton={false}
customFooter={logoFooter()}
className="payzen-update-card-modal">
{schedule && <PayzenForm onSubmit={handleCardUpdateSubmit}
onSuccess={onSuccess}
onError={handleCardUpdateError}
className="card-form"
paymentSchedule={schedule}
operator={operator}
customer={schedule.user as User}
updateCard={true}
formId={formId} >
onSuccess={onSuccess}
onError={handleCardUpdateError}
className="card-form"
paymentSchedule={schedule}
operator={operator}
customer={schedule.user as User}
updateCard={true}
formId={formId} >
{errors && <div className="payzen-errors">
{errors}
</div>}
@ -86,4 +85,4 @@ export const PayzenCardUpdateModal: React.FC<PayzenCardUpdateModalProps> = ({ is
</div>
</FabModal>
);
}
};

View File

@ -1,10 +1,9 @@
import React, { FormEvent, FunctionComponent, useEffect, useRef, useState } from 'react';
import KRGlue from "@lyracom/embedded-form-glue";
import KRGlue from '@lyracom/embedded-form-glue';
import { GatewayFormProps } from '../abstract-payment-modal';
import SettingAPI from '../../../api/setting';
import { SettingName } from '../../../models/setting';
import PayzenAPI from '../../../api/payzen';
import { Loader } from '../../base/loader';
import {
CreateTokenResponse,
KryptonClient,
@ -24,7 +23,6 @@ interface PayzenFormProps extends GatewayFormProps {
* The form validation button must be created elsewhere, using the attribute form={formId}.
*/
export const PayzenForm: React.FC<PayzenFormProps> = ({ onSubmit, onSuccess, onError, children, className, paymentSchedule, updateCard = false, cart, customer, formId }) => {
const PayZenKR = useRef<KryptonClient>(null);
const [loadingClass, setLoadingClass] = useState<'hidden' | 'loader' | 'loader-overlay'>('loader');
@ -35,14 +33,14 @@ export const PayzenForm: React.FC<PayzenFormProps> = ({ onSubmit, onSuccess, onE
KRGlue.loadLibrary(settings.get(SettingName.PayZenEndpoint), settings.get(SettingName.PayZenPublicKey))
.then(({ KR }) =>
KR.setFormConfig({
formToken: formToken.formToken,
formToken: formToken.formToken
})
)
.then(({ KR }) => KR.addForm("#payzenPaymentForm"))
.then(({ KR }) => KR.addForm('#payzenPaymentForm'))
.then(({ KR, result }) => KR.showForm(result.formId))
.then(({ KR }) => KR.onFormReady(handleFormReady))
.then(({ KR }) => KR.onFormCreated(handleFormCreated))
.then(({ KR }) => PayZenKR.current = KR);
.then(({ KR }) => { PayZenKR.current = KR; });
}).catch(error => onError(error));
});
}, [cart, paymentSchedule, customer]);
@ -59,7 +57,7 @@ export const PayzenForm: React.FC<PayzenFormProps> = ({ onSubmit, onSuccess, onE
} else {
return await PayzenAPI.chargeCreatePayment(cart, customer);
}
}
};
/**
* Callback triggered on PayZen successful payments
@ -72,11 +70,11 @@ export const PayzenForm: React.FC<PayzenFormProps> = ({ onSubmit, onSuccess, onE
const transaction = event.clientAnswer.transactions[0];
if (event.clientAnswer.orderStatus === 'PAID') {
confirmPayment(event, transaction).then((confirmation) => {
confirmPayment(event, transaction).then((confirmation) => {
PayZenKR.current.removeForms().then(() => {
onSuccess(confirmation);
});
}).catch(e => onError(e))
}).catch(e => onError(e));
} else {
const error = `${transaction?.errorMessage}. ${transaction?.detailedErrorMessage || ''}`;
onError(error || event.clientAnswer.orderStatus);
@ -95,7 +93,7 @@ export const PayzenForm: React.FC<PayzenFormProps> = ({ onSubmit, onSuccess, onE
} else {
return await PayzenAPI.confirm(event.clientAnswer.orderDetails.orderId, cart);
}
}
};
/**
* Callback triggered when the PayZen form was entirely loaded and displayed
@ -111,7 +109,7 @@ export const PayzenForm: React.FC<PayzenFormProps> = ({ onSubmit, onSuccess, onE
*/
const handleFormCreated = () => {
setLoadingClass('loader-overlay');
}
};
/**
* Callback triggered when the PayZen payment was refused
@ -120,7 +118,7 @@ export const PayzenForm: React.FC<PayzenFormProps> = ({ onSubmit, onSuccess, onE
const handleError = (answer: KryptonError) => {
const message = `${answer.errorMessage}. ${answer.detailedErrorMessage ? answer.detailedErrorMessage : ''}`;
onError(message);
}
};
/**
* Handle the submission of the form.
@ -140,7 +138,7 @@ export const PayzenForm: React.FC<PayzenFormProps> = ({ onSubmit, onSuccess, onE
// catch api errors
onError(err);
}
}
};
const Loader: FunctionComponent = () => {
return (
@ -151,7 +149,7 @@ export const PayzenForm: React.FC<PayzenFormProps> = ({ onSubmit, onSuccess, onE
};
return (
<form onSubmit={handleSubmit} id={formId} className={className ? className : ''}>
<form onSubmit={handleSubmit} id={formId} className={className || ''}>
<Loader />
<div className="container">
<div id="payzenPaymentForm" />
@ -159,4 +157,4 @@ export const PayzenForm: React.FC<PayzenFormProps> = ({ onSubmit, onSuccess, onE
{children}
</form>
);
}
};

View File

@ -66,7 +66,7 @@ const PayZenKeysFormComponent: React.FC<PayZenKeysFormProps> = ({ onValidKeys, o
useEffect(() => {
testRestApi();
}, [settings])
}, [settings]);
/**
* Assign the inputted key to the settings and check if it is valid.
@ -81,14 +81,14 @@ const PayZenKeysFormComponent: React.FC<PayZenKeysFormProps> = ({ onValidKeys, o
updateSettings(draft => draft.set(SettingName.PayZenPublicKey, key));
setPublicKeyAddOn(<i className="fa fa-check" />);
setPublicKeyAddOnClassName('key-valid');
}
};
/**
* Send a test call to the payZen REST API to check if the inputted settings key are valid.
* Depending on the test result, assign an add-on icon and a style to notify the user.
*/
const testRestApi = () => {
let valid: boolean = restApiSettings.map(s => !!settings.get(s))
const valid: boolean = restApiSettings.map(s => !!settings.get(s))
.reduce((acc, val) => acc && val, true);
if (valid && !pendingKeysValidation) {
@ -118,7 +118,7 @@ const PayZenKeysFormComponent: React.FC<PayZenKeysFormProps> = ({ onValidKeys, o
setRestApiAddOn(<i className="fa fa-times" />);
setRestApiAddOnClassName('key-invalid');
}
}
};
/**
* Assign the inputted key to the given settings
@ -126,15 +126,15 @@ const PayZenKeysFormComponent: React.FC<PayZenKeysFormProps> = ({ onValidKeys, o
const setApiKey = (setting: SettingName.PayZenUsername | SettingName.PayZenPassword | SettingName.PayZenEndpoint | SettingName.PayZenHmacKey) => {
return (key: string) => {
updateSettings(draft => draft.set(setting, key));
}
}
};
};
/**
* Check if an add-on icon must be shown for the API settings
*/
const hasApiAddOn = () => {
return restApiAddOn !== null;
}
};
return (
<div className="payzen-keys-form">
@ -147,63 +147,63 @@ const PayZenKeysFormComponent: React.FC<PayZenKeysFormProps> = ({ onValidKeys, o
<div className="payzen-public-input">
<label htmlFor="payzen_public_key">{ t('app.admin.invoices.payment.payzen.payzen_public_key') } *</label>
<FabInput id="payzen_public_key"
icon={<i className="fas fa-info" />}
defaultValue={settings.get(SettingName.PayZenPublicKey)}
onChange={testPublicKey}
addOn={publicKeyAddOn}
addOnClassName={publicKeyAddOnClassName}
debounce={200}
required />
icon={<i className="fas fa-info" />}
defaultValue={settings.get(SettingName.PayZenPublicKey)}
onChange={testPublicKey}
addOn={publicKeyAddOn}
addOnClassName={publicKeyAddOnClassName}
debounce={200}
required />
</div>
</fieldset>
<fieldset>
<legend className={hasApiAddOn() ? 'with-addon' : ''}>
<span>{t('app.admin.invoices.payment.api_keys')}</span>
{hasApiAddOn() && <span className={`fieldset-legend--addon ${restApiAddOnClassName ? restApiAddOnClassName : ''}`}>{restApiAddOn}</span>}
{hasApiAddOn() && <span className={`fieldset-legend--addon ${restApiAddOnClassName || ''}`}>{restApiAddOn}</span>}
</legend>
<div className="payzen-api-user-input">
<label htmlFor="payzen_username">{ t('app.admin.invoices.payment.payzen.payzen_username') } *</label>
<FabInput id="payzen_username"
type="number"
icon={<i className="fas fa-user-alt" />}
defaultValue={settings.get(SettingName.PayZenUsername)}
onChange={setApiKey(SettingName.PayZenUsername)}
debounce={200}
required />
type="number"
icon={<i className="fas fa-user-alt" />}
defaultValue={settings.get(SettingName.PayZenUsername)}
onChange={setApiKey(SettingName.PayZenUsername)}
debounce={200}
required />
</div>
<div className="payzen-api-password-input">
<label htmlFor="payzen_password">{ t('app.admin.invoices.payment.payzen.payzen_password') } *</label>
<FabInput id="payzen_password"
icon={<i className="fas fa-key" />}
defaultValue={settings.get(SettingName.PayZenPassword)}
onChange={setApiKey(SettingName.PayZenPassword)}
debounce={200}
required />
icon={<i className="fas fa-key" />}
defaultValue={settings.get(SettingName.PayZenPassword)}
onChange={setApiKey(SettingName.PayZenPassword)}
debounce={200}
required />
</div>
<div className="payzen-api-endpoint-input">
<label htmlFor="payzen_endpoint">{ t('app.admin.invoices.payment.payzen.payzen_endpoint') } *</label>
<FabInput id="payzen_endpoint"
type="url"
icon={<i className="fas fa-link" />}
defaultValue={settings.get(SettingName.PayZenEndpoint)}
onChange={setApiKey(SettingName.PayZenEndpoint)}
debounce={200}
required />
type="url"
icon={<i className="fas fa-link" />}
defaultValue={settings.get(SettingName.PayZenEndpoint)}
onChange={setApiKey(SettingName.PayZenEndpoint)}
debounce={200}
required />
</div>
<div className="payzen-api-hmac-input">
<label htmlFor="payzen_hmac">{ t('app.admin.invoices.payment.payzen.payzen_hmac') } *</label>
<FabInput id="payzen_hmac"
icon={<i className="fas fa-subscript" />}
defaultValue={settings.get(SettingName.PayZenHmacKey)}
onChange={setApiKey(SettingName.PayZenHmacKey)}
debounce={200}
required />
icon={<i className="fas fa-subscript" />}
defaultValue={settings.get(SettingName.PayZenHmacKey)}
onChange={setApiKey(SettingName.PayZenHmacKey)}
debounce={200}
required />
</div>
</fieldset>
</form>
</div>
);
}
};
export const PayZenKeysForm: React.FC<PayZenKeysFormProps> = ({ onValidKeys, onInvalidKeys }) => {
return (
@ -211,4 +211,4 @@ export const PayZenKeysForm: React.FC<PayZenKeysFormProps> = ({ onValidKeys, onI
<PayZenKeysFormComponent onValidKeys={onValidKeys} onInvalidKeys={onInvalidKeys} />
</Loader>
);
}
};

View File

@ -10,7 +10,6 @@ import mastercardLogo from '../../../../../images/mastercard.png';
import visaLogo from '../../../../../images/visa.png';
import { PayzenForm } from './payzen-form';
interface PayZenModalProps {
isOpen: boolean,
toggleModal: () => void,
@ -40,39 +39,39 @@ export const PayZenModal: React.FC<PayZenModalProps> = ({ isOpen, toggleModal, a
<img src={visaLogo} alt="visa" />
</div>
);
}
};
/**
* Integrates the PayzenForm into the parent PaymentModal
*/
const renderForm: FunctionComponent<GatewayFormProps> = ({ onSubmit, onSuccess, onError, operator, className, formId, cart, customer, paymentSchedule, children}) => {
const renderForm: FunctionComponent<GatewayFormProps> = ({ onSubmit, onSuccess, onError, operator, className, formId, cart, customer, paymentSchedule, children }) => {
return (
<PayzenForm onSubmit={onSubmit}
onSuccess={onSuccess}
onError={onError}
customer={customer}
operator={operator}
formId={formId}
cart={cart}
className={className}
paymentSchedule={paymentSchedule}>
onSuccess={onSuccess}
onError={onError}
customer={customer}
operator={operator}
formId={formId}
cart={cart}
className={className}
paymentSchedule={paymentSchedule}>
{children}
</PayzenForm>
);
}
};
return (
<AbstractPaymentModal isOpen={isOpen}
toggleModal={toggleModal}
logoFooter={logoFooter()}
formId="payzen-form"
formClassName="payzen-form"
className="payzen-modal"
currentUser={currentUser}
cart={cart}
customer={customer}
afterSuccess={afterSuccess}
schedule={schedule}
GatewayForm={renderForm} />
toggleModal={toggleModal}
logoFooter={logoFooter()}
formId="payzen-form"
formClassName="payzen-form"
className="payzen-modal"
currentUser={currentUser}
cart={cart}
customer={customer}
afterSuccess={afterSuccess}
schedule={schedule}
GatewayForm={renderForm} />
);
}
};

View File

@ -10,7 +10,7 @@ import { SettingName } from '../../../models/setting';
import { IApplication } from '../../../models/application';
import SettingAPI from '../../../api/setting';
declare var Application: IApplication;
declare const Application: IApplication;
interface PayzenSettingsProps {
onEditKeys: (onlinePaymentModule: { value: boolean }) => void,
@ -36,7 +36,7 @@ const icons:Map<SettingName, string> = new Map([
[SettingName.PayZenUsername, 'user'],
[SettingName.PayZenEndpoint, 'link'],
[SettingName.PayZenPublicKey, 'info']
])
]);
/**
* This component displays a summary of the PayZen account keys, with a button triggering the modal to edit them
@ -58,23 +58,22 @@ export const PayzenSettings: React.FC<PayzenSettingsProps> = ({ onEditKeys, onCu
SettingAPI.isPresent(SettingName.PayZenPassword).then(pzPassword => {
SettingAPI.isPresent(SettingName.PayZenHmacKey).then(pzHmac => {
const map = new Map(payZenKeys);
map.set(SettingName.PayZenPassword, pzPassword ? PAYZEN_HIDDEN : '');
map.set(SettingName.PayZenHmacKey, pzHmac ? PAYZEN_HIDDEN : '');
map.set(SettingName.PayZenPassword, pzPassword ? PAYZEN_HIDDEN : '');
map.set(SettingName.PayZenHmacKey, pzHmac ? PAYZEN_HIDDEN : '');
updateSettings(map);
}).catch(error => { console.error(error); })
}).catch(error => { console.error(error); });
}).catch(error => { console.error(error); });
}).catch(error => { console.error(error); });
}, []);
/**
* Callback triggered when the user clicks on the "update keys" button.
* This will open the modal dialog allowing to change the keys
*/
const handleKeysUpdate = (): void => {
onEditKeys({ value: true });
}
};
/**
* Callback triggered when the user changes the content of the currency input field.
@ -86,7 +85,7 @@ export const PayzenSettings: React.FC<PayzenSettingsProps> = ({ onEditKeys, onCu
} else {
setError(t('app.admin.invoices.payment.payzen.currency_error'));
}
}
};
/**
* Callback triggered when the user clicks on the "save currency" button.
@ -98,9 +97,9 @@ export const PayzenSettings: React.FC<PayzenSettingsProps> = ({ onEditKeys, onCu
updateSettings(draft => draft.set(SettingName.PayZenCurrency, result.value));
onCurrencyUpdateSuccess(result.value);
}, reason => {
setError(t('app.admin.invoices.payment.payzen.error_while_saving')+reason);
})
}
setError(t('app.admin.invoices.payment.payzen.error_while_saving') + reason);
});
};
return (
<div className="payzen-settings">
@ -111,11 +110,11 @@ export const PayzenSettings: React.FC<PayzenSettingsProps> = ({ onEditKeys, onCu
<div className="key-wrapper" key={setting}>
<label htmlFor={setting}>{t(`app.admin.invoices.payment.payzen.${setting}`)}</label>
<FabInput defaultValue={settings.get(setting)}
id={setting}
type={payZenPrivateSettings.indexOf(setting) > -1 ? 'password' : 'text'}
icon={<i className={`fas fa-${icons.get(setting)}`} />}
readOnly
disabled />
id={setting}
type={payZenPrivateSettings.indexOf(setting) > -1 ? 'password' : 'text'}
icon={<i className={`fas fa-${icons.get(setting)}`} />}
readOnly
disabled />
</div>
);
})}
@ -132,20 +131,19 @@ export const PayzenSettings: React.FC<PayzenSettingsProps> = ({ onEditKeys, onCu
<div className="currency-wrapper">
<label htmlFor="payzen_currency">{t('app.admin.invoices.payment.payzen.payzen_currency')}</label>
<FabInput defaultValue={settings.get(SettingName.PayZenCurrency)}
id="payzen_currency"
icon={<i className="fas fa-money-bill" />}
onChange={handleCurrencyUpdate}
maxLength={3}
pattern="[A-Z]{3}"
error={error} />
id="payzen_currency"
icon={<i className="fas fa-money-bill" />}
onChange={handleCurrencyUpdate}
maxLength={3}
pattern="[A-Z]{3}"
error={error} />
</div>
<FabButton className="save-currency" onClick={saveCurrency}>{t('app.admin.invoices.payment.payzen.save')}</FabButton>
</div>
</div>
</div>
);
}
};
const PayzenSettingsWrapper: React.FC<PayzenSettingsProps> = ({ onEditKeys, onCurrencyUpdateSuccess }) => {
return (
@ -153,6 +151,6 @@ const PayzenSettingsWrapper: React.FC<PayzenSettingsProps> = ({ onEditKeys, onCu
<PayzenSettings onEditKeys={onEditKeys} onCurrencyUpdateSuccess={onCurrencyUpdateSuccess} />
</Loader>
);
}
};
Application.Components.component('payzenSettings', react2angular(PayzenSettingsWrapper, ['onEditKeys', 'onCurrencyUpdateSuccess']));

View File

@ -36,15 +36,14 @@ export const StripeCardUpdateModal: React.FC<StripeCardUpdateModalProps> = ({ is
<img src={visaLogo} alt="visa" />
</div>
);
}
};
/**
* When the user clicks the submit button, we disable it to prevent double form submission
*/
const handleCardUpdateSubmit = (): void => {
setCanSubmitUpdateCard(false);
}
};
/**
* When the card was not updated, show the error
@ -52,21 +51,21 @@ export const StripeCardUpdateModal: React.FC<StripeCardUpdateModalProps> = ({ is
const handleCardUpdateError = (error): void => {
setErrors(error);
setCanSubmitUpdateCard(true);
}
};
return (
<FabModal title={t('app.shared.stripe_card_update_modal.update_card')}
isOpen={isOpen}
toggleModal={toggleModal}
closeButton={false}
customFooter={logoFooter()}
className="stripe-update-card-modal">
isOpen={isOpen}
toggleModal={toggleModal}
closeButton={false}
customFooter={logoFooter()}
className="stripe-update-card-modal">
{schedule && <StripeCardUpdate onSubmit={handleCardUpdateSubmit}
onSuccess={onSuccess}
onError={handleCardUpdateError}
schedule={schedule}
operator={operator}
className="card-form" >
onSuccess={onSuccess}
onError={handleCardUpdateError}
schedule={schedule}
operator={operator}
className="card-form" >
{errors && <div className="stripe-errors">
{errors}
</div>}
@ -81,4 +80,4 @@ export const StripeCardUpdateModal: React.FC<StripeCardUpdateModalProps> = ({ is
</div>
</FabModal>
);
}
};

View File

@ -19,7 +19,6 @@ interface StripeCardUpdateProps {
* The form validation button must be created elsewhere, using the attribute form="stripe-card".
*/
export const StripeCardUpdate: React.FC<StripeCardUpdateProps> = ({ onSubmit, onSuccess, onError, className, schedule, operator, children }) => {
const stripe = useStripe();
const elements = useElements();
@ -37,7 +36,7 @@ export const StripeCardUpdate: React.FC<StripeCardUpdateProps> = ({ onSubmit, on
const cardElement = elements.getElement(CardElement);
const { error, paymentMethod } = await stripe.createPaymentMethod({
type: 'card',
card: cardElement,
card: cardElement
});
if (error) {
@ -46,8 +45,8 @@ export const StripeCardUpdate: React.FC<StripeCardUpdateProps> = ({ onSubmit, on
} else {
try {
// we start by associating the payment method with the user
const { client_secret } = await StripeAPI.setupIntent(schedule.user.id);
const { error } = await stripe.confirmCardSetup(client_secret, {
const intent = await StripeAPI.setupIntent(schedule.user.id);
const { error } = await stripe.confirmCardSetup(intent.client_secret, {
payment_method: paymentMethod.id,
mandate_data: {
customer_acceptance: {
@ -58,7 +57,7 @@ export const StripeCardUpdate: React.FC<StripeCardUpdateProps> = ({ onSubmit, on
}
}
}
})
});
if (error) {
onError(error.message);
} else {
@ -75,7 +74,7 @@ export const StripeCardUpdate: React.FC<StripeCardUpdateProps> = ({ onSubmit, on
onError(err);
}
}
}
};
/**
* Options for the Stripe's card input
@ -90,7 +89,7 @@ export const StripeCardUpdate: React.FC<StripeCardUpdateProps> = ({ onSubmit, on
invalid: {
color: '#9e2146',
iconColor: '#9e2146'
},
}
},
hidePostalCode: true
};
@ -101,4 +100,4 @@ export const StripeCardUpdate: React.FC<StripeCardUpdateProps> = ({ onSubmit, on
{children}
</form>
);
}
};

View File

@ -25,7 +25,7 @@ export const StripeConfirm: React.FC<StripeConfirmProps> = ({ clientSecret, onRe
* When the component is mounted, run the 3DS confirmation.
*/
useEffect(() => {
stripe.confirmCardPayment(clientSecret).then(function(result) {
stripe.confirmCardPayment(clientSecret).then(function (result) {
onResponse();
if (result.error) {
// Display error.message in your UI.
@ -42,4 +42,4 @@ export const StripeConfirm: React.FC<StripeConfirmProps> = ({ clientSecret, onRe
return <div className="stripe-confirm">
<div className={`message--${type}`}><span className="message-text">{message}</span></div>
</div>;
}
};

View File

@ -1,6 +1,6 @@
import React, { memo, useEffect, useState } from 'react';
import { Elements } from '@stripe/react-stripe-js';
import { loadStripe } from "@stripe/stripe-js";
import { loadStripe } from '@stripe/stripe-js';
import { SettingName } from '../../../models/setting';
import SettingAPI from '../../../api/setting';
@ -18,7 +18,7 @@ export const StripeElements: React.FC = memo(({ children }) => {
const promise = loadStripe(key.value);
setStripe(promise);
});
}, [])
}, []);
return (
<div>
@ -27,4 +27,6 @@ export const StripeElements: React.FC = memo(({ children }) => {
</Elements>}
</div>
);
})
});
StripeElements.displayName = 'StripeElements';

View File

@ -11,7 +11,6 @@ import { Invoice } from '../../../models/invoice';
* The form validation button must be created elsewhere, using the attribute form={formId}.
*/
export const StripeForm: React.FC<GatewayFormProps> = ({ onSubmit, onSuccess, onError, children, className, paymentSchedule = false, cart, customer, operator, formId }) => {
const { t } = useTranslation('shared');
const stripe = useStripe();
@ -31,7 +30,7 @@ export const StripeForm: React.FC<GatewayFormProps> = ({ onSubmit, onSuccess, on
const cardElement = elements.getElement(CardElement);
const { error, paymentMethod } = await stripe.createPaymentMethod({
type: 'card',
card: cardElement,
card: cardElement
});
if (error) {
@ -45,8 +44,8 @@ export const StripeForm: React.FC<GatewayFormProps> = ({ onSubmit, onSuccess, on
await handleServerConfirmation(res);
} else {
// we start by associating the payment method with the user
const { client_secret } = await StripeAPI.setupIntent(customer.id);
const { setupIntent, error } = await stripe.confirmCardSetup(client_secret, {
const intent = await StripeAPI.setupIntent(customer.id);
const { setupIntent, error } = await stripe.confirmCardSetup(intent.client_secret, {
payment_method: paymentMethod.id,
mandate_data: {
customer_acceptance: {
@ -57,7 +56,7 @@ export const StripeForm: React.FC<GatewayFormProps> = ({ onSubmit, onSuccess, on
}
}
}
})
});
if (error) {
onError(error.message);
} else {
@ -71,7 +70,7 @@ export const StripeForm: React.FC<GatewayFormProps> = ({ onSubmit, onSuccess, on
onError(err);
}
}
}
};
/**
* Process the server response about the Strong-customer authentication (SCA)
@ -105,8 +104,7 @@ export const StripeForm: React.FC<GatewayFormProps> = ({ onSubmit, onSuccess, on
} else {
console.error(`[StripeForm] unknown response received: ${response}`);
}
}
};
/**
* Options for the Stripe's card input
@ -121,15 +119,15 @@ export const StripeForm: React.FC<GatewayFormProps> = ({ onSubmit, onSuccess, on
invalid: {
color: '#9e2146',
iconColor: '#9e2146'
},
}
},
hidePostalCode: true
};
return (
<form onSubmit={handleSubmit} id={formId} className={className ? className : ''}>
<form onSubmit={handleSubmit} id={formId} className={className || ''}>
<CardElement options={cardOptions} />
{children}
</form>
);
}
};

View File

@ -7,7 +7,6 @@ import { SettingName } from '../../../models/setting';
import StripeAPI from '../../../api/external/stripe';
import SettingAPI from '../../../api/setting';
interface StripeKeysFormProps {
onValidKeys: (stripePublic: string, stripeSecret:string) => void,
onInvalidKeys: () => void,
@ -67,7 +66,6 @@ const StripeKeysFormComponent: React.FC<StripeKeysFormProps> = ({ onValidKeys, o
}
}, [publicKeyAddOnClassName, secretKeyAddOnClassName]);
/**
* Send a test call to the Stripe API to check if the inputted public key is valid
*/
@ -93,7 +91,7 @@ const StripeKeysFormComponent: React.FC<StripeKeysFormProps> = ({ onValidKeys, o
setPublicKeyAddOnClassName('key-invalid');
}
});
}
};
/**
* Send a test call to the Stripe API to check if the inputted secret key is valid
@ -120,7 +118,7 @@ const StripeKeysFormComponent: React.FC<StripeKeysFormProps> = ({ onValidKeys, o
setSecretKeyAddOnClassName('key-invalid');
}
});
}
};
return (
<div className="stripe-keys-form">
@ -131,29 +129,29 @@ const StripeKeysFormComponent: React.FC<StripeKeysFormProps> = ({ onValidKeys, o
<div className="stripe-public-input">
<label htmlFor="stripe_public_key">{ t('app.admin.invoices.payment.public_key') } *</label>
<FabInput id="stripe_public_key"
icon={<i className="fa fa-info" />}
defaultValue={publicKey}
onChange={testPublicKey}
addOn={publicKeyAddOn}
addOnClassName={publicKeyAddOnClassName}
debounce={200}
required />
icon={<i className="fa fa-info" />}
defaultValue={publicKey}
onChange={testPublicKey}
addOn={publicKeyAddOn}
addOnClassName={publicKeyAddOnClassName}
debounce={200}
required />
</div>
<div className="stripe-secret-input">
<label htmlFor="stripe_secret_key">{ t('app.admin.invoices.payment.secret_key') } *</label>
<FabInput id="stripe_secret_key"
icon={<i className="fa fa-key" />}
defaultValue={secretKey}
onChange={testSecretKey}
addOn={secretKeyAddOn}
addOnClassName={secretKeyAddOnClassName}
debounce={200}
required/>
icon={<i className="fa fa-key" />}
defaultValue={secretKey}
onChange={testSecretKey}
addOn={secretKeyAddOn}
addOnClassName={secretKeyAddOnClassName}
debounce={200}
required/>
</div>
</form>
</div>
);
}
};
export const StripeKeysForm: React.FC<StripeKeysFormProps> = ({ onValidKeys, onInvalidKeys }) => {
return (
@ -161,4 +159,4 @@ export const StripeKeysForm: React.FC<StripeKeysFormProps> = ({ onValidKeys, onI
<StripeKeysFormComponent onValidKeys={onValidKeys} onInvalidKeys={onInvalidKeys} />
</Loader>
);
}
};

View File

@ -11,7 +11,6 @@ import mastercardLogo from '../../../../../images/mastercard.png';
import visaLogo from '../../../../../images/visa.png';
import { Invoice } from '../../../models/invoice';
interface StripeModalProps {
isOpen: boolean,
toggleModal: () => void,
@ -42,41 +41,41 @@ export const StripeModal: React.FC<StripeModalProps> = ({ isOpen, toggleModal, a
<img src={visaLogo} alt="visa" />
</div>
);
}
};
/**
* Integrates the StripeForm into the parent PaymentModal
*/
const renderForm: FunctionComponent<GatewayFormProps> = ({ onSubmit, onSuccess, onError, operator, className, formId, cart, customer, paymentSchedule, children}) => {
const renderForm: FunctionComponent<GatewayFormProps> = ({ onSubmit, onSuccess, onError, operator, className, formId, cart, customer, paymentSchedule, children }) => {
return (
<StripeElements>
<StripeForm onSubmit={onSubmit}
onSuccess={onSuccess}
onError={onError}
operator={operator}
className={className}
formId={formId}
cart={cart}
customer={customer}
paymentSchedule={paymentSchedule}>
onSuccess={onSuccess}
onError={onError}
operator={operator}
className={className}
formId={formId}
cart={cart}
customer={customer}
paymentSchedule={paymentSchedule}>
{children}
</StripeForm>
</StripeElements>
);
}
};
return (
<AbstractPaymentModal className="stripe-modal"
isOpen={isOpen}
toggleModal={toggleModal}
logoFooter={logoFooter()}
formId="stripe-form"
formClassName="stripe-form"
currentUser={currentUser}
cart={cart}
customer={customer}
afterSuccess={afterSuccess}
schedule={schedule}
GatewayForm={renderForm} />
isOpen={isOpen}
toggleModal={toggleModal}
logoFooter={logoFooter()}
formId="stripe-form"
formClassName="stripe-form"
currentUser={currentUser}
cart={cart}
customer={customer}
afterSuccess={afterSuccess}
schedule={schedule}
GatewayForm={renderForm} />
);
}
};

View File

@ -6,7 +6,6 @@ import { User } from '../../models/user';
import { PaymentSchedule } from '../../models/payment-schedule';
import { useTranslation } from 'react-i18next';
interface UpdateCardModalProps {
isOpen: boolean,
toggleModal: () => void,
@ -16,7 +15,6 @@ interface UpdateCardModalProps {
operator: User
}
/**
* This component open a modal dialog for the configured payment gateway, allowing the user to input his card data
* to process an online payment.
@ -38,22 +36,22 @@ const UpdateCardModalComponent: React.FC<UpdateCardModalProps> = ({ isOpen, togg
*/
const renderStripeModal = (): ReactElement => {
return <StripeCardUpdateModal isOpen={isOpen}
toggleModal={toggleModal}
onSuccess={afterSuccess}
operator={operator}
schedule={schedule} />
}
toggleModal={toggleModal}
onSuccess={afterSuccess}
operator={operator}
schedule={schedule} />;
};
/**
* Render the PayZen update-card modal
*/ // 1
const renderPayZenModal = (): ReactElement => {
return <PayzenCardUpdateModal isOpen={isOpen}
toggleModal={toggleModal}
onSuccess={afterSuccess}
operator={operator}
schedule={schedule} />
}
toggleModal={toggleModal}
onSuccess={afterSuccess}
operator={operator}
schedule={schedule} />;
};
/**
* Determine which gateway is in use with the current schedule and return the appropriate modal
@ -71,8 +69,7 @@ const UpdateCardModalComponent: React.FC<UpdateCardModalProps> = ({ isOpen, togg
console.error(`[UpdateCardModal] unexpected gateway: ${schedule.gateway_subscription?.classname}`);
return <div />;
}
}
};
export const UpdateCardModal: React.FC<UpdateCardModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, operator, schedule }) => {
return (
@ -80,4 +77,4 @@ export const UpdateCardModal: React.FC<UpdateCardModalProps> = ({ isOpen, toggle
<UpdateCardModalComponent isOpen={isOpen} toggleModal={toggleModal} afterSuccess={afterSuccess} onError={onError} operator={operator} schedule={schedule} />
</Loader>
);
}
};

View File

@ -8,7 +8,6 @@ import { LabelledInput } from '../base/labelled-input';
import { Loader } from '../base/loader';
import { FabAlert } from '../base/fab-alert';
interface CreatePlanCategoryProps {
onSuccess: (message: string) => void,
onError: (message: string) => void,
@ -51,7 +50,7 @@ const CreatePlanCategoryComponent: React.FC<CreatePlanCategoryProps> = ({ onSucc
* We update the name of the temporary-set plan-category, accordingly.
*/
const onCategoryNameChange = (event: BaseSyntheticEvent) => {
setCategory({...category, name: event.target.value });
setCategory({ ...category, name: event.target.value });
};
/**
@ -59,7 +58,7 @@ const CreatePlanCategoryComponent: React.FC<CreatePlanCategoryProps> = ({ onSucc
* We update the weight of the temporary-set plan-category, accordingly.
*/
const onCategoryWeightChange = (event: BaseSyntheticEvent) => {
setCategory({...category, weight: event.target.value });
setCategory({ ...category, weight: event.target.value });
};
/**
@ -74,44 +73,44 @@ const CreatePlanCategoryComponent: React.FC<CreatePlanCategoryProps> = ({ onSucc
*/
const resetCategory = () => {
setCategory(null);
}
};
return (
<div className="create-plan-category">
<FabButton type='button'
icon={<i className='fa fa-plus' />}
className="add-category"
onClick={toggleModal}>
icon={<i className='fa fa-plus' />}
className="add-category"
onClick={toggleModal}>
{t('app.admin.create_plan_category.new_category')}
</FabButton>
<FabModal title={t('app.admin.create_plan_category.new_category')}
className="create-plan-category-modal"
isOpen={isOpen}
toggleModal={toggleModal}
closeButton={true}
confirmButton={t('app.admin.create_plan_category.confirm_create')}
onConfirm={onCreateConfirmed}
onCreation={initCategoryCreation}>
className="create-plan-category-modal"
isOpen={isOpen}
toggleModal={toggleModal}
closeButton={true}
confirmButton={t('app.admin.create_plan_category.confirm_create')}
onConfirm={onCreateConfirmed}
onCreation={initCategoryCreation}>
{category && <div>
<label htmlFor="name">{t('app.admin.create_plan_category.name')}</label>
<LabelledInput id="name"
label={<i className="fa fa-tag" />}
type="text"
value={category.name}
onChange={onCategoryNameChange} />
label={<i className="fa fa-tag" />}
type="text"
value={category.name}
onChange={onCategoryNameChange} />
<label htmlFor="weight">{t('app.admin.create_plan_category.significance')}</label>
<LabelledInput id="weight"
type="number"
label={<i className="fa fa-sort-numeric-desc" />}
value={category.weight}
onChange={onCategoryWeightChange} />
type="number"
label={<i className="fa fa-sort-numeric-desc" />}
value={category.weight}
onChange={onCategoryWeightChange} />
</div>}
<FabAlert level="info" className="significance-info">
{t('app.admin.create_plan_category.significance_info')}
</FabAlert>
</FabModal>
</div>
)
);
};
export const CreatePlanCategory: React.FC<CreatePlanCategoryProps> = ({ onSuccess, onError }) => {
@ -120,4 +119,4 @@ export const CreatePlanCategory: React.FC<CreatePlanCategoryProps> = ({ onSucces
<CreatePlanCategoryComponent onSuccess={onSuccess} onError={onError} />
</Loader>
);
}
};

View File

@ -6,7 +6,6 @@ import { FabButton } from '../base/fab-button';
import { FabModal } from '../base/fab-modal';
import { Loader } from '../base/loader';
interface DeletePlanCategoryProps {
onSuccess: (message: string) => void,
onError: (message: string) => void,
@ -46,22 +45,21 @@ const DeletePlanCategoryComponent: React.FC<DeletePlanCategoryProps> = ({ onSucc
<div className="delete-plan-category">
<FabButton type='button' className="delete-button" icon={<i className="fa fa-trash" />} onClick={toggleDeletionModal} />
<FabModal title={t('app.admin.delete_plan_category.delete_category')}
isOpen={deletionModal}
toggleModal={toggleDeletionModal}
closeButton={true}
confirmButton={t('app.admin.delete_plan_category.confirm_delete')}
onConfirm={onDeleteConfirmed}>
isOpen={deletionModal}
toggleModal={toggleDeletionModal}
closeButton={true}
confirmButton={t('app.admin.delete_plan_category.confirm_delete')}
onConfirm={onDeleteConfirmed}>
<span>{t('app.admin.delete_plan_category.delete_confirmation')}</span>
</FabModal>
</div>
)
);
};
export const DeletePlanCategory: React.FC<DeletePlanCategoryProps> = ({ onSuccess, onError, category }) => {
return (
<Loader>
<DeletePlanCategoryComponent onSuccess={onSuccess} onError={onError} category={category} />
</Loader>
);
}
};

View File

@ -8,7 +8,6 @@ import { LabelledInput } from '../base/labelled-input';
import { Loader } from '../base/loader';
import { FabAlert } from '../base/fab-alert';
interface EditPlanCategoryProps {
onSuccess: (message: string) => void,
onError: (message: string) => void,
@ -53,7 +52,7 @@ const EditPlanCategoryComponent: React.FC<EditPlanCategoryProps> = ({ onSuccess,
* We update the name of the temporary-set plan-category, accordingly.
*/
const onCategoryNameChange = (event: BaseSyntheticEvent) => {
setTempCategory({...tempCategory, name: event.target.value });
setTempCategory({ ...tempCategory, name: event.target.value });
};
/**
@ -61,48 +60,45 @@ const EditPlanCategoryComponent: React.FC<EditPlanCategoryProps> = ({ onSuccess,
* We update the weight of the temporary-set plan-category, accordingly.
*/
const onCategoryWeightChange = (event: BaseSyntheticEvent) => {
setTempCategory({...tempCategory, weight: event.target.value });
setTempCategory({ ...tempCategory, weight: event.target.value });
};
return (
<div className="edit-plan-category">
<FabButton type='button' className="edit-button" icon={<i className="fa fa-edit" />} onClick={toggleEditionModal} />
<FabModal title={t('app.admin.edit_plan_category.edit_category')}
isOpen={editionModal}
toggleModal={toggleEditionModal}
className="edit-plan-category-modal"
closeButton={true}
confirmButton={t('app.admin.edit_plan_category.confirm_edition')}
onConfirm={onEditConfirmed}>
isOpen={editionModal}
toggleModal={toggleEditionModal}
className="edit-plan-category-modal"
closeButton={true}
confirmButton={t('app.admin.edit_plan_category.confirm_edition')}
onConfirm={onEditConfirmed}>
{tempCategory && <div>
<label htmlFor="category-name">{t('app.admin.edit_plan_category.name')}</label>
<LabelledInput id="category-name"
type="text"
label={<i className="fa fa-tag" />}
value={tempCategory.name}
onChange={onCategoryNameChange} />
type="text"
label={<i className="fa fa-tag" />}
value={tempCategory.name}
onChange={onCategoryNameChange} />
<label htmlFor="category-weight">{t('app.admin.edit_plan_category.significance')}</label>
<LabelledInput id="category-weight"
type="number"
label={<i className="fa fa-sort-numeric-desc" />}
value={tempCategory.weight}
onChange={onCategoryWeightChange} />
type="number"
label={<i className="fa fa-sort-numeric-desc" />}
value={tempCategory.weight}
onChange={onCategoryWeightChange} />
</div>}
<FabAlert level="info" className="significance-info">
{t('app.admin.edit_plan_category.significance_info')}
</FabAlert>
</FabModal>
</div>
)
);
};
export const EditPlanCategory: React.FC<EditPlanCategoryProps> = ({ onSuccess, onError, category }) => {
return (
<Loader>
<EditPlanCategoryComponent onSuccess={onSuccess} onError={onError} category={category} />
</Loader>
);
}
};

View File

@ -9,7 +9,7 @@ import { CreatePlanCategory } from './create-plan-category';
import { EditPlanCategory } from './edit-plan-category';
import { DeletePlanCategory } from './delete-plan-category';
declare var Application: IApplication;
declare const Application: IApplication;
interface PlanCategoriesListProps {
onSuccess: (message: string) => void,
@ -52,9 +52,9 @@ export const PlanCategoriesList: React.FC<PlanCategoriesListProps> = ({ onSucces
return (
<div className="plan-categories-list">
<CreatePlanCategory onSuccess={handleSuccess}
onError={onError} />
onError={onError} />
<h3>{t('app.admin.plan_categories_list.categories_list')}</h3>
{categories && categories.length == 0 && <span>{t('app.admin.plan_categories_list.no_categories')}</span>}
{categories && categories.length === 0 && <span>{t('app.admin.plan_categories_list.no_categories')}</span>}
{categories && categories.length > 0 && <table className="categories-table">
<thead>
<tr>
@ -75,16 +75,15 @@ export const PlanCategoriesList: React.FC<PlanCategoriesListProps> = ({ onSucces
</tbody>
</table>}
</div>
)
);
};
const PlanCategoriesListWrapper: React.FC<PlanCategoriesListProps> = ({ onSuccess, onError }) => {
return (
<Loader>
<PlanCategoriesList onSuccess={onSuccess} onError={onError} />
</Loader>
);
}
};
Application.Components.component('planCategoriesList', react2angular(PlanCategoriesListWrapper, ['onSuccess', 'onError']));

View File

@ -1,14 +1,14 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import moment from 'moment';
import _ from 'lodash'
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 var Fablab: IFablab;
declare let Fablab: IFablab;
interface PlanCardProps {
plan: Plan,
@ -29,82 +29,82 @@ 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 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 new Intl.NumberFormat(Fablab.intl_locale, {style: 'currency', currency: Fablab.intl_currency}).format(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")
*/
const duration = (): string => {
return moment.duration(plan.interval_count, plan.interval).humanize();
}
};
/**
* Check if no users are currently logged-in
*/
const mustLogin = (): boolean => {
return _.isNil(operator);
}
};
/**
* Check if the user can subscribe to the current plan, for himself
*/
const canSubscribeForMe = (): boolean => {
return operator?.role === UserRole.Member || (operator?.role === UserRole.Manager && userId === operator?.id)
}
return operator?.role === UserRole.Member || (operator?.role === UserRole.Manager && userId === operator?.id);
};
/**
* Check if the user can subscribe to the current plan, for someone else
*/
const canSubscribeForOther = (): boolean => {
return operator?.role === UserRole.Admin || (operator?.role === UserRole.Manager && userId !== operator?.id)
}
return operator?.role === UserRole.Admin || (operator?.role === UserRole.Manager && userId !== operator?.id);
};
/**
* Check it the user has subscribed to this plan or not
*/
const hasSubscribedToThisPlan = (): boolean => {
return subscribedPlanId === plan.id;
}
};
/**
* Check if the plan has an attached file
*/
const hasAttachment = (): boolean => {
return !!plan.plan_file_url;
}
};
/**
* Check if the plan has a description
*/
const hasDescription = (): boolean => {
return !!plan.description;
}
};
/**
* Check if the plan is allowing a monthly payment schedule
*/
const canBeScheduled = (): boolean => {
return plan.monthly_payment;
}
};
/**
* Callback triggered when the user select the plan
*/
const handleSelectPlan = (): void => {
onSelectPlan(plan);
}
};
/**
* Callback triggered when a visitor (not logged-in user) select a plan
*/
const handleLoginRequest = (): void => {
onLoginRequested();
}
};
return (
<div className="plan-card">
<h3 className="title">{plan.base_name}</h3>
<div className="content">
{canBeScheduled() && <div className="wrap-monthly">
<div className="price">
<div className="amount">{t('app.public.plans.AMOUNT_per_month', {AMOUNT: monthlyAmount()})}</div>
<div className="amount">{t('app.public.plans.AMOUNT_per_month', { AMOUNT: monthlyAmount() })}</div>
<span className="period">{duration()}</span>
</div>
</div>}
@ -116,15 +116,15 @@ const PlanCardComponent: React.FC<PlanCardProps> = ({ plan, userId, subscribedPl
</div>}
</div>
<div className="card-footer">
{hasDescription() && <div className="plan-description" dangerouslySetInnerHTML={{__html: plan.description}}/>}
{hasAttachment() && <a className="info-link" href={ plan.plan_file_url } target="_blank">{ t('app.public.plans.more_information') }</a>}
{hasDescription() && <div className="plan-description" dangerouslySetInnerHTML={{ __html: plan.description }}/>}
{hasAttachment() && <a className="info-link" href={ plan.plan_file_url } target="_blank" rel="noreferrer">{ t('app.public.plans.more_information') }</a>}
{mustLogin() && <div className="cta-button">
<button className="subscribe-button" onClick={handleLoginRequest}>{t('app.public.plans.i_subscribe_online')}</button>
</div>}
{canSubscribeForMe() && <div className="cta-button">
{!hasSubscribedToThisPlan() && <button className={`subscribe-button ${isSelected ? 'selected-card' : ''}`}
onClick={handleSelectPlan}
disabled={!_.isNil(subscribedPlanId)}>
onClick={handleSelectPlan}
disabled={!_.isNil(subscribedPlanId)}>
{t('app.public.plans.i_choose_that_plan')}
</button>}
{hasSubscribedToThisPlan() && <button className="subscribe-button selected-card" disabled>
@ -133,15 +133,15 @@ const PlanCardComponent: React.FC<PlanCardProps> = ({ plan, userId, subscribedPl
</div>}
{canSubscribeForOther() && <div className="cta-button">
<button className={`subscribe-button ${isSelected ? 'selected-card' : ''}`}
onClick={handleSelectPlan}
disabled={_.isNil(userId)}>
onClick={handleSelectPlan}
disabled={_.isNil(userId)}>
<span>{ t('app.public.plans.i_choose_that_plan') }</span>
</button>
</div>}
</div>
</div>
);
}
};
export const PlanCard: React.FC<PlanCardProps> = ({ plan, userId, subscribedPlanId, operator, onSelectPlan, isSelected, onLoginRequested }) => {
return (
@ -149,4 +149,4 @@ export const PlanCard: React.FC<PlanCardProps> = ({ plan, userId, subscribedPlan
<PlanCardComponent plan={plan} userId={userId} subscribedPlanId={subscribedPlanId} operator={operator} isSelected={isSelected} onSelectPlan={onSelectPlan} onLoginRequested={onLoginRequested}/>
</Loader>
);
}
};

View File

@ -37,9 +37,9 @@ export const PlansFilter: React.FC<PlansFilterProps> = ({ user, groups, onGroupS
*/
const buildGroupOptions = (): Array<selectOption> => {
return groups.filter(g => !g.disabled && g.slug !== 'admins').map(g => {
return { value: g.id, label: g.name }
return { value: g.id, label: g.name };
});
}
};
/**
* Convert all durations to the react-select format
@ -50,40 +50,40 @@ export const PlansFilter: React.FC<PlansFilterProps> = ({ user, groups, onGroupS
});
options.unshift({ value: null, label: t('app.public.plans_filter.all_durations') });
return options;
}
};
/**
* Callback triggered when the user selects a group in the dropdown list
*/
const handleGroupSelected = (option: selectOption): void => {
onGroupSelected(option.value);
}
};
/**
* Callback triggered when the user selects a duration in the dropdown list
*/
const handleDurationSelected = (option: selectOption): void => {
onDurationSelected(durations[option.value]?.plans_ids);
}
};
return (
<div className="plans-filter">
{!user && <div className="group-filter">
<label htmlFor="group">{t('app.public.plans_filter.i_am')}</label>
<Select placeholder={t('app.public.plans_filter.select_group')}
id="group"
className="group-select"
onChange={handleGroupSelected}
options={buildGroupOptions()}/>
id="group"
className="group-select"
onChange={handleGroupSelected}
options={buildGroupOptions()}/>
</div>}
{durations && <div className="duration-filter">
<label htmlFor="duration">{t('app.public.plans_filter.i_want_duration')}</label>
<Select placeholder={t('app.public.plans_filter.select_duration')}
id="duration"
className="duration-select"
onChange={handleDurationSelected}
options={buildDurationOptions()}/>
id="duration"
className="duration-select"
onChange={handleDurationSelected}
options={buildDurationOptions()}/>
</div>}
</div>
)
}
);
};

View File

@ -11,10 +11,9 @@ import { PlanCard } from './plan-card';
import { Loader } from '../base/loader';
import { react2angular } from 'react2angular';
import { IApplication } from '../../models/application';
import { useTranslation } from 'react-i18next';
import { PlansFilter } from './plans-filter';
declare var Application: IApplication;
declare const Application: IApplication;
interface PlansListProps {
onError: (message: string) => void,
@ -32,8 +31,6 @@ type PlansTree = Map<number, Map<number, Array<Plan>>>;
* This component display an organized list of plans to allow the end-user to select one and subscribe online
*/
const PlansList: React.FC<PlansListProps> = ({ onError, onPlanSelection, onLoginRequest, operator, customer, subscribedPlanId }) => {
const { t } = useTranslation('public');
// all plans
const [plans, setPlans] = useState<PlansTree>(null);
// all plan-categories, ordered by weight
@ -59,7 +56,7 @@ const PlansList: React.FC<PlansListProps> = ({ onError, onPlanSelection, onLogin
.then(data => setPlans(sortPlans(data, groupsData)))
.catch(error => onError(error));
})
.catch(error => onError(error))
.catch(error => onError(error));
}, []);
// reset the selected plan when the user changes
@ -99,7 +96,7 @@ const PlansList: React.FC<PlansListProps> = ({ onError, onPlanSelection, onLogin
}
}
return res;
}
};
/**
* Filter the plans to display, depending on the connected/selected user and on the selected group filter (if any)
@ -109,28 +106,28 @@ const PlansList: React.FC<PlansListProps> = ({ onError, onPlanSelection, onLogin
if (groupFilter) return new Map([[groupFilter, plans.get(groupFilter)]]);
return new Map([[customer.group_id, plans.get(customer.group_id)]]);
}
};
/**
* When called with a group ID, returns the name of the requested group
*/
const groupName = (groupId: number): string => {
return groups.find(g => g.id === groupId)?.name;
}
};
/**
* When called with a category ID, returns the name of the requested plan-category
*/
const categoryName = (categoryId: number): string => {
return planCategories.find(c => c.id === categoryId)?.name;
}
};
/**
* Check if the currently selected plan matched the provided one
*/
const isSelectedPlan = (plan: Plan): boolean => {
return (plan === selectedPlan);
}
};
/**
* Callback for sorting plans by weight
@ -138,7 +135,7 @@ const PlansList: React.FC<PlansListProps> = ({ onError, onPlanSelection, onLogin
*/
const comparePlans = (plan1: Plan, plan2: Plan): number => {
return (plan2.ui_weight - plan1.ui_weight);
}
};
/**
* Callback for sorting categories by weight
@ -151,7 +148,7 @@ const PlansList: React.FC<PlansListProps> = ({ onError, onPlanSelection, onLogin
const categoryObject1 = planCategories.find(c => c.id === category1[0]);
const categoryObject2 = planCategories.find(c => c.id === category2[0]);
return (categoryObject2.weight - categoryObject1.weight);
}
};
/**
* Callback triggered when the user chooses a plan to subscribe
@ -159,21 +156,21 @@ const PlansList: React.FC<PlansListProps> = ({ onError, onPlanSelection, onLogin
const handlePlanSelection = (plan: Plan): void => {
setSelectedPlan(plan);
onPlanSelection(plan);
}
};
/**
* Callback triggered when the user selects a group to filter the current list
*/
const handleFilterByGroup = (groupId: number): void => {
setGroupFilter(groupId);
}
};
/**
* Callback triggered when the user selects a duration to filter the current list
*/
const handleFilterByDuration = (plansIds: Array<number>): void => {
setPlansFilter(plansIds);
}
};
/**
* Callback for filtering plans to display, depending on the filter-by-plans-ids selection
@ -183,7 +180,7 @@ const PlansList: React.FC<PlansListProps> = ({ onError, onPlanSelection, onLogin
if (!plansFilter) return true;
return plansFilter.includes(plan.id);
}
};
/**
* Render the provided list of categories, with each associated plans
@ -194,15 +191,15 @@ const PlansList: React.FC<PlansListProps> = ({ onError, onPlanSelection, onLogin
{Array.from(plans).sort(compareCategories).map(([categoryId, plansByCategory]) => {
const categoryPlans = plansByCategory.filter(filterPlan);
return (
<div key={categoryId} className={`plans-per-category ${categoryId ? 'with-category' : 'no-category' }`}>
<div key={categoryId} className={`plans-per-category ${categoryId ? 'with-category' : 'no-category'}`}>
{!!categoryId && categoryPlans.length > 0 && <h3 className="category-title">{ categoryName(categoryId) }</h3>}
{renderPlans(categoryPlans)}
</div>
)
);
})}
</div>
);
}
};
/**
* Render the provided list of plans, ordered by ui_weight.
@ -212,17 +209,17 @@ const PlansList: React.FC<PlansListProps> = ({ onError, onPlanSelection, onLogin
<div className="list-of-plans">
{categoryPlans.length > 0 && categoryPlans.sort(comparePlans).map(plan => (
<PlanCard key={plan.id}
userId={customer?.id}
subscribedPlanId={subscribedPlanId}
plan={plan}
operator={operator}
isSelected={isSelectedPlan(plan)}
onSelectPlan={handlePlanSelection}
onLoginRequested={onLoginRequest} />
userId={customer?.id}
subscribedPlanId={subscribedPlanId}
plan={plan}
operator={operator}
isSelected={isSelectedPlan(plan)}
onSelectPlan={handlePlanSelection}
onLoginRequested={onLoginRequest} />
))}
</div>
);
}
};
return (
<div className="plans-list">
@ -233,12 +230,11 @@ const PlansList: React.FC<PlansListProps> = ({ onError, onPlanSelection, onLogin
<h2 className="group-title">{ groupName(groupId) }</h2>
{plansByGroup && renderPlansByCategory(plansByGroup)}
</div>
)
);
})}
</div>
);
}
};
const PlansListWrapper: React.FC<PlansListProps> = ({ customer, onError, onPlanSelection, onLoginRequest, operator, subscribedPlanId }) => {
return (
@ -246,6 +242,6 @@ const PlansListWrapper: React.FC<PlansListProps> = ({ customer, onError, onPlanS
<PlansList customer={customer} onError={onError} onPlanSelection={onPlanSelection} onLoginRequest={onLoginRequest} operator={operator} subscribedPlanId={subscribedPlanId} />
</Loader>
);
}
};
Application.Components.component('plansList', react2angular(PlansListWrapper, ['customer', 'onError', 'onPlanSelection', 'onLoginRequest', 'operator', 'subscribedPlanId']));

View File

@ -15,7 +15,7 @@ import { IApplication } from '../../models/application';
import { PrepaidPack } from '../../models/prepaid-pack';
import PrepaidPackAPI from '../../api/prepaid-pack';
declare var Application: IApplication;
declare const Application: IApplication;
type PackableItem = Machine;
@ -65,7 +65,7 @@ const PacksSummaryComponent: React.FC<PacksSummaryProps> = ({ item, itemType, cu
PrepaidPackAPI.index({ priceable_id: item.id, priceable_type: itemType, group_id: customer.group_id, disabled: false })
.then(data => setPacks(data))
.catch(error => onError(error));
}
};
/**
* Total of minutes used by the customer
@ -74,7 +74,7 @@ const PacksSummaryComponent: React.FC<PacksSummaryProps> = ({ item, itemType, cu
if (!userPacks) return 0;
return userPacks.map(up => up.minutes_used).reduce((acc, curr) => acc + curr, 0);
}
};
/**
* Total of minutes available is the packs bought by the customer
@ -83,34 +83,34 @@ const PacksSummaryComponent: React.FC<PacksSummaryProps> = ({ item, itemType, cu
if (!userPacks) return 0;
return userPacks.map(up => up.prepaid_pack.minutes).reduce((acc, curr) => acc + curr, 0);
}
};
/**
* Total prepaid hours remaining for the current customer
*/
const totalHours = (): number => {
return (totalAvailable() - totalUsed()) / 60;
}
};
/**
* Do we need to display the "buy new pack" button?
*/
const shouldDisplayButton = (): boolean => {
if (!packs?.length) return false;
if (!packs?.length) return false;
if (threshold < 1) {
return totalAvailable() - totalUsed() <= totalAvailable() * threshold;
}
return totalAvailable() - totalUsed() <= threshold * 60;
}
};
/**
* Open/closes the prepaid-pack buying modal
*/
const togglePacksModal = (): void => {
setPacksModal(!packsModal);
}
};
/**
* Callback triggered when the customer has successfully bought a prepaid-pack
@ -121,7 +121,7 @@ const PacksSummaryComponent: React.FC<PacksSummaryProps> = ({ item, itemType, cu
UserPackAPI.index({ user_id: customer.id, priceable_type: itemType, priceable_id: item.id })
.then(data => setUserPacks(data))
.catch(error => onError(error));
}
};
// prevent component rendering if no customer selected
if (_.isEmpty(customer)) return <div />;
@ -141,19 +141,19 @@ const PacksSummaryComponent: React.FC<PacksSummaryProps> = ({ item, itemType, cu
{t('app.logged.packs_summary.buy_a_new_pack')}
</FabButton>
<ProposePacksModal isOpen={packsModal}
toggleModal={togglePacksModal}
item={item}
itemType={itemType}
customer={customer}
operator={operator}
onError={onError}
onDecline={togglePacksModal}
onSuccess={handlePackBoughtSuccess} />
toggleModal={togglePacksModal}
item={item}
itemType={itemType}
customer={customer}
operator={operator}
onError={onError}
onDecline={togglePacksModal}
onSuccess={handlePackBoughtSuccess} />
</div>}
</div>
</div>
);
}
};
export const PacksSummary: React.FC<PacksSummaryProps> = ({ item, itemType, customer, operator, onError, onSuccess, refresh }) => {
return (
@ -161,6 +161,6 @@ export const PacksSummary: React.FC<PacksSummaryProps> = ({ item, itemType, cust
<PacksSummaryComponent item={item} itemType={itemType} customer={customer} operator={operator} onError={onError} onSuccess={onSuccess} refresh={refresh} />
</Loader>
);
}
};
Application.Components.component('packsSummary', react2angular(PacksSummary, ['item', 'itemType', 'customer', 'operator', 'onError', 'onSuccess', 'refresh']));

View File

@ -14,7 +14,6 @@ import UserLib from '../../lib/user';
import { LocalPaymentModal } from '../payment/local-payment/local-payment-modal';
import FormatLib from '../../lib/format';
type PackableItem = Machine;
interface ProposePacksModalProps {
@ -50,20 +49,19 @@ export const ProposePacksModal: React.FC<ProposePacksModalProps> = ({ isOpen, to
.catch(error => onError(error));
}, [item]);
/**
* Open/closes the payment modal
*/
const togglePaymentModal = (): void => {
setPaymentModal(!paymentModal);
}
};
/**
* Open/closes the local payment modal (for admins and managers)
*/
const toggleLocalPaymentModal = (): void => {
setLocalPaymentModal(!localPaymentModal);
}
};
/**
* Convert the hourly-based price of the given prive, to a total price, based on the duration of the given pack
@ -71,14 +69,14 @@ export const ProposePacksModal: React.FC<ProposePacksModalProps> = ({ isOpen, to
const hourlyPriceToTotal = (price: Price, pack: PrepaidPack): number => {
const hours = pack.minutes / 60;
return price.amount * hours;
}
};
/**
* Return the number of hours, user-friendly formatted
*/
const formatDuration = (minutes: number): string => {
return t('app.logged.propose_packs_modal.pack_DURATION', { DURATION: minutes / 60 });
}
};
/**
* Return a user-friendly string for the validity of the provided pack
@ -86,14 +84,14 @@ export const ProposePacksModal: React.FC<ProposePacksModalProps> = ({ isOpen, to
const formatValidity = (pack: PrepaidPack): string => {
const period = t(`app.logged.propose_packs_modal.period.${pack.validity_interval}`, { COUNT: pack.validity_count });
return t('app.logged.propose_packs_modal.validity', { COUNT: pack.validity_count, PERIODS: period });
}
};
/**
* The user has declined to buy a pack
*/
const handlePacksRefused = (): void => {
onDecline(item);
}
};
/**
* The user has accepted to buy the provided pack, process with the payment
@ -104,22 +102,22 @@ export const ProposePacksModal: React.FC<ProposePacksModalProps> = ({ isOpen, to
customer_id: customer.id,
payment_method: PaymentMethod.Card,
items: [
{ prepaid_pack: { id: pack.id }}
{ prepaid_pack: { id: pack.id } }
]
});
if (new UserLib(operator).isPrivileged(customer)) {
return toggleLocalPaymentModal();
}
togglePaymentModal();
}
}
};
};
/**
* Callback triggered when the user has bought the pack with a successful payment
*/
const handlePackBought = (): void => {
onSuccess(t('app.logged.propose_packs_modal.pack_bought_success'), item);
}
};
/**
* Render the given prepaid-pack
@ -127,7 +125,7 @@ export const ProposePacksModal: React.FC<ProposePacksModalProps> = ({ isOpen, to
const renderPack = (pack: PrepaidPack) => {
if (!price) return;
const normalPrice = hourlyPriceToTotal(price, pack)
const normalPrice = hourlyPriceToTotal(price, pack);
return (
<div key={pack.id} className="pack">
<span className="duration">{formatDuration(pack.minutes)}</span>
@ -138,36 +136,36 @@ export const ProposePacksModal: React.FC<ProposePacksModalProps> = ({ isOpen, to
{t('app.logged.propose_packs_modal.buy_this_pack')}
</FabButton>
</div>
)
}
);
};
return (
<FabModal isOpen={isOpen}
toggleModal={toggleModal}
width={ModalSize.large}
confirmButton={t('app.logged.propose_packs_modal.no_thanks')}
onConfirm={handlePacksRefused}
className="propose-packs-modal"
title={t('app.logged.propose_packs_modal.available_packs')}>
toggleModal={toggleModal}
width={ModalSize.large}
confirmButton={t('app.logged.propose_packs_modal.no_thanks')}
onConfirm={handlePacksRefused}
className="propose-packs-modal"
title={t('app.logged.propose_packs_modal.available_packs')}>
<p>{t('app.logged.propose_packs_modal.packs_proposed')}</p>
<div className="list-of-packs">
{packs?.map(p => renderPack(p))}
</div>
{cart && <div>
<PaymentModal isOpen={paymentModal}
toggleModal={togglePaymentModal}
afterSuccess={handlePackBought}
onError={onError}
cart={cart}
currentUser={operator}
customer={customer} />
toggleModal={togglePaymentModal}
afterSuccess={handlePackBought}
onError={onError}
cart={cart}
currentUser={operator}
customer={customer} />
<LocalPaymentModal isOpen={localPaymentModal}
toggleModal={toggleLocalPaymentModal}
afterSuccess={handlePackBought}
cart={cart}
currentUser={operator}
customer={customer} />
toggleModal={toggleLocalPaymentModal}
afterSuccess={handlePackBought}
cart={cart}
currentUser={operator}
customer={customer} />
</div>}
</FabModal>
);
}
};

View File

@ -26,28 +26,20 @@ export const ConfigurePacksButton: React.FC<ConfigurePacksButtonProps> = ({ pack
const [packs, setPacks] = useState<Array<PrepaidPack>>(packsData);
const [showList, setShowList] = useState<boolean>(false);
const [editPackModal, setEditPackModal] = useState<boolean>(false);
/**
* Return the number of hours, user-friendly formatted
*/
const formatDuration = (minutes: number): string => {
return t('app.admin.configure_packs_button.pack_DURATION', { DURATION: minutes / 60 });
}
};
/**
* Open/closes the popover listing the existing packs
*/
const toggleShowList = (): void => {
setShowList(!showList);
}
/**
* Open/closes the "edit pack" modal
*/
const toggleEditPackModal = (): void => {
setEditPackModal(!editPackModal);
}
};
/**
* Callback triggered when the PrepaidPack was successfully created/deleted/updated.
@ -58,18 +50,18 @@ export const ConfigurePacksButton: React.FC<ConfigurePacksButtonProps> = ({ pack
PrepaidPackAPI.index({ group_id: groupId, priceable_id: priceableId, priceable_type: priceableType })
.then(data => setPacks(data))
.catch(error => onError(error));
}
};
/**
* Render the button used to trigger the "new pack" modal
*/
const renderAddButton = (): ReactNode => {
return <CreatePack onSuccess={handleSuccess}
onError={onError}
groupId={groupId}
priceableId={priceableId}
priceableType={priceableType} />;
}
onError={onError}
groupId={groupId}
priceableId={priceableId}
priceableType={priceableType} />;
};
return (
<div className="configure-packs-button">
@ -91,4 +83,4 @@ export const ConfigurePacksButton: React.FC<ConfigurePacksButtonProps> = ({ pack
</FabPopover>}
</div>
);
}
};

View File

@ -28,7 +28,7 @@ export const CreatePack: React.FC<CreatePackProps> = ({ onSuccess, onError, grou
*/
const toggleModal = (): void => {
setIsOpen(!isOpen);
}
};
/**
* Callback triggered when the user has validated the creation of the new PrepaidPack
@ -47,18 +47,18 @@ export const CreatePack: React.FC<CreatePackProps> = ({ onSuccess, onError, grou
toggleModal();
})
.catch(error => onError(error));
}
};
return (
<div className="create-pack">
<button className="add-pack-button" onClick={toggleModal}><i className="fas fa-plus"/></button>
<FabModal isOpen={isOpen}
toggleModal={toggleModal}
title={t('app.admin.create_pack.new_pack')}
className="new-pack-modal"
closeButton
confirmButton={t('app.admin.create_pack.create_pack')}
onConfirmSendFormId="new-pack">
toggleModal={toggleModal}
title={t('app.admin.create_pack.new_pack')}
className="new-pack-modal"
closeButton
confirmButton={t('app.admin.create_pack.create_pack')}
onConfirmSendFormId="new-pack">
<FabAlert level="info">
{t('app.admin.create_pack.new_pack_info', { TYPE: priceableType })}
</FabAlert>
@ -66,4 +66,4 @@ export const CreatePack: React.FC<CreatePackProps> = ({ onSuccess, onError, grou
</FabModal>
</div>
);
}
};

View File

@ -6,7 +6,6 @@ import { Loader } from '../base/loader';
import { PrepaidPack } from '../../models/prepaid-pack';
import PrepaidPackAPI from '../../api/prepaid-pack';
interface DeletePackProps {
onSuccess: (message: string) => void,
onError: (message: string) => void,
@ -46,22 +45,21 @@ const DeletePackComponent: React.FC<DeletePackProps> = ({ onSuccess, onError, pa
<div className="delete-pack">
<FabButton type='button' className="remove-pack-button" icon={<i className="fa fa-trash" />} onClick={toggleDeletionModal} />
<FabModal title={t('app.admin.delete_pack.delete_pack')}
isOpen={deletionModal}
toggleModal={toggleDeletionModal}
closeButton={true}
confirmButton={t('app.admin.delete_pack.confirm_delete')}
onConfirm={onDeleteConfirmed}>
isOpen={deletionModal}
toggleModal={toggleDeletionModal}
closeButton={true}
confirmButton={t('app.admin.delete_pack.confirm_delete')}
onConfirm={onDeleteConfirmed}>
<span>{t('app.admin.delete_pack.delete_confirmation')}</span>
</FabModal>
</div>
)
);
};
export const DeletePack: React.FC<DeletePackProps> = ({ onSuccess, onError, pack }) => {
return (
<Loader>
<DeletePackComponent onSuccess={onSuccess} onError={onError} pack={pack} />
</Loader>
);
}
};

View File

@ -27,7 +27,7 @@ export const EditPack: React.FC<EditPackProps> = ({ pack, onSuccess, onError })
*/
const toggleModal = (): void => {
setIsOpen(!isOpen);
}
};
/**
* When the user clicks on the edition button, query the full data of the current pack from the API, then open te edition modal
@ -39,7 +39,7 @@ export const EditPack: React.FC<EditPackProps> = ({ pack, onSuccess, onError })
toggleModal();
})
.catch(error => onError(error));
}
};
/**
* Callback triggered when the user has validated the changes of the PrepaidPack
@ -51,20 +51,20 @@ export const EditPack: React.FC<EditPackProps> = ({ pack, onSuccess, onError })
toggleModal();
})
.catch(error => onError(error));
}
};
return (
<div className="edit-pack">
<FabButton type='button' className="edit-pack-button" icon={<i className="fas fa-edit" />} onClick={handleRequestEdit} />
<FabModal isOpen={isOpen}
toggleModal={toggleModal}
title={t('app.admin.edit_pack.edit_pack')}
className="edit-pack-modal"
closeButton
confirmButton={t('app.admin.edit_pack.confirm_changes')}
onConfirmSendFormId="edit-pack">
toggleModal={toggleModal}
title={t('app.admin.edit_pack.edit_pack')}
className="edit-pack-modal"
closeButton
confirmButton={t('app.admin.edit_pack.confirm_changes')}
onConfirmSendFormId="edit-pack">
{packData && <PackForm formId="edit-pack" onSubmit={handleUpdate} pack={packData} />}
</FabModal>
</div>
);
}
};

View File

@ -5,7 +5,7 @@ import { FabButton } from '../base/fab-button';
import { Price } from '../../models/price';
import FormatLib from '../../lib/format';
declare var Fablab: IFablab;
declare let Fablab: IFablab;
interface EditablePriceProps {
price: Price,
@ -28,14 +28,14 @@ export const EditablePrice: React.FC<EditablePriceProps> = ({ price, onSave }) =
newPrice.amount = parseFloat(tempPrice);
onSave(newPrice);
toggleEdit();
}
};
/**
* Enable or disable the edit mode
*/
const toggleEdit = (): void => {
setEdit(!edit);
}
};
return (
<span className="editable-price">
@ -47,4 +47,4 @@ export const EditablePrice: React.FC<EditablePriceProps> = ({ price, onSave }) =
</span>}
</span>
);
}
};

View File

@ -18,8 +18,8 @@ import PrepaidPackAPI from '../../api/prepaid-pack';
import { PrepaidPack } from '../../models/prepaid-pack';
import { useImmer } from 'use-immer';
declare var Fablab: IFablab;
declare var Application: IApplication;
declare let Fablab: IFablab;
declare const Application: IApplication;
interface MachinesPricingProps {
onError: (message: string) => void,
@ -42,7 +42,7 @@ const MachinesPricing: React.FC<MachinesPricingProps> = ({ onError, onSuccess })
MachineAPI.index({ disabled: false })
.then(data => setMachines(data))
.catch(error => onError(error));
GroupAPI.index({ disabled: false , admins: false })
GroupAPI.index({ disabled: false, admins: false })
.then(data => setGroups(data))
.catch(error => onError(error));
PriceAPI.index({ priceable_type: 'Machine', plan_id: null })
@ -50,7 +50,7 @@ const MachinesPricing: React.FC<MachinesPricingProps> = ({ onError, onSuccess })
.catch(error => onError(error));
PrepaidPackAPI.index()
.then(data => setPacks(data))
.catch(error => onError(error))
.catch(error => onError(error));
}, []);
// duration of the example slot
@ -93,7 +93,7 @@ const MachinesPricing: React.FC<MachinesPricingProps> = ({ onError, onSuccess })
draft[index] = price;
return draft;
});
}
};
/**
* Callback triggered when the user has confirmed to update a price
@ -104,8 +104,8 @@ const MachinesPricing: React.FC<MachinesPricingProps> = ({ onError, onSuccess })
onSuccess(t('app.admin.machines_pricing.price_updated'));
updatePrice(price);
})
.catch(error => onError(error))
}
.catch(error => onError(error));
};
return (
<div className="machines-pricing">
@ -122,23 +122,23 @@ const MachinesPricing: React.FC<MachinesPricingProps> = ({ onError, onSuccess })
</tr>
</thead>
<tbody>
{machines?.map(machine => <tr key={machine.id}>
<td>{machine.name}</td>
{groups?.map(group => <td key={group.id}>
{prices && <EditablePrice price={findPriceBy(machine.id, group.id)} onSave={handleUpdatePrice} />}
{packs && <ConfigurePacksButton packsData={filterPacksBy(machine.id, group.id)}
onError={onError}
onSuccess={onSuccess}
groupId={group.id}
priceableId={machine.id}
priceableType="Machine" />}
</td>)}
</tr>)}
{machines?.map(machine => <tr key={machine.id}>
<td>{machine.name}</td>
{groups?.map(group => <td key={group.id}>
{prices && <EditablePrice price={findPriceBy(machine.id, group.id)} onSave={handleUpdatePrice} />}
{packs && <ConfigurePacksButton packsData={filterPacksBy(machine.id, group.id)}
onError={onError}
onSuccess={onSuccess}
groupId={group.id}
priceableId={machine.id}
priceableType="Machine" />}
</td>)}
</tr>)}
</tbody>
</table>
</div>
);
}
};
const MachinesPricingWrapper: React.FC<MachinesPricingProps> = ({ onError, onSuccess }) => {
return (
@ -146,8 +146,6 @@ const MachinesPricingWrapper: React.FC<MachinesPricingProps> = ({ onError, onSuc
<MachinesPricing onError={onError} onSuccess={onSuccess} />
</Loader>
);
}
};
Application.Components.component('machinesPricing', react2angular(MachinesPricingWrapper, ['onError', 'onSuccess']));

View File

@ -7,7 +7,7 @@ import { useImmer } from 'use-immer';
import { FabInput } from '../base/fab-input';
import { IFablab } from '../../models/fablab';
declare var Fablab: IFablab;
declare let Fablab: IFablab;
interface PackFormProps {
formId: string,
@ -38,7 +38,7 @@ export const PackForm: React.FC<PackFormProps> = ({ formId, onSubmit, pack }) =>
*/
const buildOptions = (): Array<selectOption> => {
return ALL_INTERVALS.map(i => intervalToOption(i));
}
};
/**
* Convert the given validity-interval to the react-select format
@ -47,7 +47,7 @@ export const PackForm: React.FC<PackFormProps> = ({ formId, onSubmit, pack }) =>
if (!value) return { value, label: '' };
return { value, label: t(`app.admin.pack_form.intervals.${value}`, { COUNT: packData.validity_count || 0 }) };
}
};
/**
* Callback triggered when the user sends the form.
@ -55,7 +55,7 @@ export const PackForm: React.FC<PackFormProps> = ({ formId, onSubmit, pack }) =>
const handleSubmit = (event: BaseSyntheticEvent): void => {
event.preventDefault();
onSubmit(packData);
}
};
/**
* Callback triggered when the user inputs an amount for the current pack.
@ -64,7 +64,7 @@ export const PackForm: React.FC<PackFormProps> = ({ formId, onSubmit, pack }) =>
updatePackData(draft => {
draft.amount = parseFloat(amount);
});
}
};
/**
* Callback triggered when the user inputs a number of hours for the current pack.
@ -73,7 +73,7 @@ export const PackForm: React.FC<PackFormProps> = ({ formId, onSubmit, pack }) =>
updatePackData(draft => {
draft.minutes = parseInt(hours, 10) * 60;
});
}
};
/**
* Callback triggered when the user inputs a number of periods for the current pack.
@ -82,7 +82,7 @@ export const PackForm: React.FC<PackFormProps> = ({ formId, onSubmit, pack }) =>
updatePackData(draft => {
draft.validity_count = parseInt(count, 10);
});
}
};
/**
* Callback triggered when the user selects a type of interval for the current pack.
@ -91,7 +91,7 @@ export const PackForm: React.FC<PackFormProps> = ({ formId, onSubmit, pack }) =>
updatePackData(draft => {
draft.validity_interval = option.value as interval;
});
}
};
/**
* Callback triggered when the user disables the pack.
@ -99,42 +99,42 @@ export const PackForm: React.FC<PackFormProps> = ({ formId, onSubmit, pack }) =>
const handleUpdateDisabled = (checked: boolean) => {
updatePackData(draft => {
draft.disabled = checked;
})
}
});
};
return (
<form id={formId} onSubmit={handleSubmit} className="pack-form">
<label htmlFor="hours">{t('app.admin.pack_form.hours')} *</label>
<FabInput id="hours"
type="number"
defaultValue={packData?.minutes / 60 || ''}
onChange={handleUpdateHours}
min={1}
icon={<i className="fas fa-clock" />}
required />
type="number"
defaultValue={packData?.minutes / 60 || ''}
onChange={handleUpdateHours}
min={1}
icon={<i className="fas fa-clock" />}
required />
<label htmlFor="amount">{t('app.admin.pack_form.amount')} *</label>
<FabInput id="amount"
type="number"
step={0.01}
min={0}
defaultValue={packData?.amount || ''}
onChange={handleUpdateAmount}
icon={<i className="fas fa-money-bill" />}
addOn={Fablab.intl_currency}
required />
type="number"
step={0.01}
min={0}
defaultValue={packData?.amount || ''}
onChange={handleUpdateAmount}
icon={<i className="fas fa-money-bill" />}
addOn={Fablab.intl_currency}
required />
<label htmlFor="validity_count">{t('app.admin.pack_form.validity_count')}</label>
<div className="interval-inputs">
<FabInput id="validity_count"
type="number"
min={0}
defaultValue={packData?.validity_count || ''}
onChange={handleUpdateValidityCount}
icon={<i className="fas fa-calendar-week" />} />
type="number"
min={0}
defaultValue={packData?.validity_count || ''}
onChange={handleUpdateValidityCount}
icon={<i className="fas fa-calendar-week" />} />
<Select placeholder={t('app.admin.pack_form.select_interval')}
className="select-interval"
defaultValue={intervalToOption(packData?.validity_interval)}
onChange={handleUpdateValidityInterval}
options={buildOptions()} />
className="select-interval"
defaultValue={intervalToOption(packData?.validity_interval)}
onChange={handleUpdateValidityInterval}
options={buildOptions()} />
</div>
<label htmlFor="disabled">{t('app.admin.pack_form.disabled')}</label>
<div>
@ -142,4 +142,4 @@ export const PackForm: React.FC<PackFormProps> = ({ formId, onSubmit, pack }) =>
</div>
</form>
);
}
};

View File

@ -16,8 +16,7 @@ import { SettingBulkResult, SettingName } from '../models/setting';
import { IApplication } from '../models/application';
import SettingAPI from '../api/setting';
declare var Application: IApplication;
declare const Application: IApplication;
interface SelectGatewayModalModalProps {
isOpen: boolean,
@ -37,8 +36,8 @@ const SelectGatewayModal: React.FC<SelectGatewayModalModalProps> = ({ isOpen, to
// request the configured gateway to the API
useEffect(() => {
SettingAPI.get(SettingName.PaymentGateway).then(gateway => {
setSelectedGateway(gateway.value ? gateway.value : '');
})
setSelectedGateway(gateway.value ? gateway.value : '');
});
}, []);
/**
@ -48,7 +47,7 @@ const SelectGatewayModal: React.FC<SelectGatewayModalModalProps> = ({ isOpen, to
setPreventConfirmGateway(true);
updateSettings();
setPreventConfirmGateway(false);
}
};
/**
* Save the gateway provided by the target input into the component state
@ -56,14 +55,14 @@ const SelectGatewayModal: React.FC<SelectGatewayModalModalProps> = ({ isOpen, to
const setGateway = (event: BaseSyntheticEvent) => {
const gateway = event.target.value;
setSelectedGateway(gateway);
}
};
/**
* Check if any payment gateway was selected
*/
const hasSelectedGateway = (): boolean => {
return selectedGateway !== '';
}
};
/**
* Callback triggered when the embedded form has validated all the stripe keys
@ -76,7 +75,7 @@ const SelectGatewayModal: React.FC<SelectGatewayModalModalProps> = ({ isOpen, to
return newMap;
});
setPreventConfirmGateway(false);
}
};
/**
* Callback triggered when the embedded form has validated all the PayZen keys
@ -84,14 +83,14 @@ const SelectGatewayModal: React.FC<SelectGatewayModalModalProps> = ({ isOpen, to
const handleValidPayZenKeys = (payZenKeys: Map<SettingName, string>): void => {
setGatewayConfig(payZenKeys);
setPreventConfirmGateway(false);
}
};
/**
* Callback triggered when the embedded form has not validated all keys
*/
const handleInvalidKeys = (): void => {
setPreventConfirmGateway(true);
}
};
/**
* Send the new gateway settings to the API to save them
@ -111,17 +110,17 @@ const SelectGatewayModal: React.FC<SelectGatewayModalModalProps> = ({ isOpen, to
}, reason => {
onError(reason);
});
}
};
return (
<FabModal title={t('app.admin.invoices.payment.gateway_modal.select_gateway_title')}
isOpen={isOpen}
toggleModal={toggleModal}
width={ModalSize.medium}
className="gateway-modal"
confirmButton={t('app.admin.invoices.payment.gateway_modal.confirm_button')}
onConfirm={onGatewayConfirmed}
preventConfirm={preventConfirmGateway}>
isOpen={isOpen}
toggleModal={toggleModal}
width={ModalSize.medium}
className="gateway-modal"
confirmButton={t('app.admin.invoices.payment.gateway_modal.confirm_button')}
onConfirm={onGatewayConfirmed}
preventConfirm={preventConfirmGateway}>
{!hasSelectedGateway() && <p className="info-gateway">
{t('app.admin.invoices.payment.gateway_modal.gateway_info')}
</p>}
@ -143,6 +142,6 @@ const SelectGatewayModalWrapper: React.FC<SelectGatewayModalModalProps> = ({ isO
<SelectGatewayModal isOpen={isOpen} toggleModal={toggleModal} currentUser={currentUser} onSuccess={onSuccess} onError={onError} />
</Loader>
);
}
};
Application.Components.component('selectGatewayModal', react2angular(SelectGatewayModalWrapper, ['isOpen', 'toggleModal', 'currentUser', 'onSuccess', 'onError']));

View File

@ -12,18 +12,17 @@ interface AvatarProps {
* This component renders the user-profile's picture or a placeholder
*/
export const Avatar: React.FC<AvatarProps> = ({ user, className }) => {
/**
* Check if the provided user has a configured avatar
*/
const hasAvatar = (): boolean => {
return !!user?.profile?.user_avatar?.attachment_url;
}
};
return (
<div className={`avatar ${className ? className : ''}`}>
<div className={`avatar ${className || ''}`}>
{!hasAvatar() && <img src={noAvatar} alt="avatar placeholder"/>}
{hasAvatar() && <img src={user.profile.user_avatar.attachment_url} alt="user's avatar"/>}
</div>
);
}
};

View File

@ -10,7 +10,7 @@ import WalletLib from '../lib/wallet';
import { ShoppingCart } from '../models/payment';
import FormatLib from '../lib/format';
declare var Application: IApplication;
declare const Application: IApplication;
interface WalletInfoProps {
cart: ShoppingCart,
@ -39,27 +39,27 @@ export const WalletInfo: React.FC<WalletInfoProps> = ({ cart, currentUser, walle
* If the currently connected user (i.e. the operator), is an admin or a manager, he may book the reservation for someone else.
*/
const isOperatorAndClient = (): boolean => {
return currentUser.id == cart.customer_id;
}
return currentUser.id === cart.customer_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 cart.items.find(i => 'subscription' in i) && cart.payment_schedule;
}
};
/**
* Return the human-readable name of the item currently bought with the wallet
*/
@ -74,15 +74,15 @@ export const WalletInfo: React.FC<WalletInfoProps> = ({ cart, currentUser, walle
}
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: FormatLib.price(wallet.amount)})}</h3>
<h3>{t('app.shared.wallet.wallet_info.you_have_AMOUNT_in_wallet', { AMOUNT: FormatLib.price(wallet.amount) })}</h3>
{!hasRemainingPrice() && <p>
{t('app.shared.wallet.wallet_info.wallet_pay_ITEM', {ITEM: getPriceItem()})}
{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', {
@ -92,9 +92,9 @@ export const WalletInfo: React.FC<WalletInfoProps> = ({ cart, currentUser, walle
</p>}
</div>}
{!isOperatorAndClient() && <div>
<h3>{t('app.shared.wallet.wallet_info.client_have_AMOUNT_in_wallet', {AMOUNT: FormatLib.price(wallet.amount)})}</h3>
<h3>{t('app.shared.wallet.wallet_info.client_have_AMOUNT_in_wallet', { AMOUNT: FormatLib.price(wallet.amount) })}</h3>
{!hasRemainingPrice() && <p>
{t('app.shared.wallet.wallet_info.client_wallet_pay_ITEM', {ITEM: getPriceItem()})}
{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', {
@ -110,7 +110,7 @@ export const WalletInfo: React.FC<WalletInfoProps> = ({ cart, currentUser, walle
</div>}
</div>
);
}
};
const WalletInfoWrapper: React.FC<WalletInfoProps> = ({ currentUser, cart, price, wallet }) => {
return (
@ -118,6 +118,6 @@ const WalletInfoWrapper: React.FC<WalletInfoProps> = ({ currentUser, cart, price
<WalletInfo currentUser={currentUser} cart={cart} price={price} wallet={wallet}/>
</Loader>
);
}
};
Application.Components.component('walletInfo', react2angular(WalletInfoWrapper, ['currentUser', 'price', 'cart', 'wallet']));

View File

@ -17,8 +17,8 @@
// list of supported authentication methods
const METHODS = {
'DatabaseProvider': 'local_database',
'OAuth2Provider': 'o_auth2'
DatabaseProvider: 'local_database',
OAuth2Provider: 'o_auth2'
};
/**
@ -38,7 +38,7 @@ const findIdxById = function (elements, id) {
* @returns {Boolean} true if the mapping is declared
*/
const check_oauth2_id_is_mapped = function (mappings) {
for (let mapping of Array.from(mappings)) {
for (const mapping of Array.from(mappings)) {
if ((mapping.local_model === 'user') && (mapping.local_field === 'uid') && !mapping._destroy) {
return true;
}
@ -78,9 +78,9 @@ class AuthenticationController {
/**
* Return a localized string for the provided method
*/
$scope.methodName = function(method) {
$scope.methodName = function (method) {
return _t('app.shared.authentication.' + METHODS[method]);
}
};
/**
* Open a modal allowing to specify the data mapping for the given field
@ -92,7 +92,7 @@ class AuthenticationController {
resolve: {
field () { return mapping; },
datatype () {
for (let field of Array.from($scope.mappingFields[mapping.local_model])) {
for (const field of Array.from($scope.mappingFields[mapping.local_model])) {
if (field[0] === mapping.local_field) {
return field[1];
}
@ -149,7 +149,7 @@ class AuthenticationController {
$scope.cancel = function () { $uibModalInstance.dismiss(); };
}]
})
.result['finally'](null).then(function (transfo_rules) { mapping.transformation = transfo_rules; });
.result.finally(null).then(function (transfo_rules) { mapping.transformation = transfo_rules; });
};
}
}
@ -247,7 +247,7 @@ Application.Controllers.controller('NewAuthenticationController', ['$scope', '$s
// === OAuth2Provider ===
if ($scope.provider.providable_type === 'OAuth2Provider') {
if (typeof $scope.provider.providable_attributes.o_auth2_mappings_attributes === 'undefined') {
return $scope.provider.providable_attributes['o_auth2_mappings_attributes'] = [];
return $scope.provider.providable_attributes.o_auth2_mappings_attributes = [];
}
}
};

View File

@ -328,15 +328,15 @@ Application.Controllers.controller('GraphsController', ['$scope', '$state', '$ro
var getRankingLabel = function (key, typeKey) {
if ($scope.selectedIndex) {
if (typeKey === 'subType') {
for (let type of Array.from($scope.selectedIndex.types)) {
for (let subtype of Array.from(type.subtypes)) {
for (const type of Array.from($scope.selectedIndex.types)) {
for (const subtype of Array.from(type.subtypes)) {
if (subtype.key === key) {
return subtype.label;
}
}
}
} else {
for (let field of Array.from($scope.selectedIndex.additional_fields)) {
for (const field of Array.from($scope.selectedIndex.additional_fields)) {
if (field.key === typeKey) {
switch (field.data_type) {
case 'date': return moment(key).format('LL'); break;
@ -370,7 +370,7 @@ Application.Controllers.controller('GraphsController', ['$scope', '$state', '$ro
if (index.graph.chart_type !== 'discreteBarChart') {
// list statistics types
const stat_types = [];
for (let t of Array.from(index.types)) {
for (const t of Array.from(index.types)) {
if (t.graph) {
stat_types.push(t.key);
}
@ -430,15 +430,15 @@ Application.Controllers.controller('GraphsController', ['$scope', '$state', '$ro
// run query
return es.search({
'index': 'stats',
'type': esType,
'searchType': 'query_then_fetch',
'size': 0,
index: 'stats',
type: esType,
searchType: 'query_then_fetch',
size: 0,
'stat-type': statType,
'custom-query': '',
'start-date': moment($scope.datePickerStart.selected).format(),
'end-date': moment($scope.datePickerEnd.selected).format(),
'body': buildElasticAggregationsQuery(statType, $scope.display.interval, moment($scope.datePickerStart.selected), moment($scope.datePickerEnd.selected))
body: buildElasticAggregationsQuery(statType, $scope.display.interval, moment($scope.datePickerStart.selected), moment($scope.datePickerEnd.selected))
}
, function (error, response) {
if (error) {
@ -468,11 +468,11 @@ Application.Controllers.controller('GraphsController', ['$scope', '$state', '$ro
// run query
return es.search({
'index': 'stats',
'type': esType,
'searchType': 'query_then_fetch',
'size': 0,
'body': buildElasticAggregationsRankingQuery(groupKey, sortKey, moment($scope.datePickerStart.selected), moment($scope.datePickerEnd.selected))
index: 'stats',
type: esType,
searchType: 'query_then_fetch',
size: 0,
body: buildElasticAggregationsRankingQuery(groupKey, sortKey, moment($scope.datePickerStart.selected), moment($scope.datePickerEnd.selected))
}
, function (error, response) {
if (error) {
@ -487,7 +487,7 @@ Application.Controllers.controller('GraphsController', ['$scope', '$state', '$ro
* Parse a final elastic results bucket and return a D3 compatible object
* @param bucket {{key_as_string:{String}, key:{Number}, doc_count:{Number}, total:{{value:{Number}}}}} interval bucket
*/
const parseElasticBucket = bucket => [ bucket.key, bucket.total.value ];
const parseElasticBucket = bucket => [bucket.key, bucket.total.value];
/**
* Build an object representing the content of the REST-JSON query to elasticSearch, based on the parameters
@ -499,45 +499,45 @@ Application.Controllers.controller('GraphsController', ['$scope', '$state', '$ro
*/
var buildElasticAggregationsQuery = function (type, interval, intervalBegin, intervalEnd) {
const q = {
'query': {
'bool': {
'must': [
query: {
bool: {
must: [
{
'match': {
'type': type
match: {
type: type
}
},
{
'range': {
'date': {
'gte': intervalBegin.format(),
'lte': intervalEnd.format()
range: {
date: {
gte: intervalBegin.format(),
lte: intervalEnd.format()
}
}
}
]
}
},
'aggregations': {
'subgroups': {
'terms': {
'field': 'subType'
aggregations: {
subgroups: {
terms: {
field: 'subType'
}, // TODO allow aggregate by custom field
'aggregations': {
'intervals': {
'date_histogram': {
'field': 'date',
'interval': interval,
'min_doc_count': 0,
'extended_bounds': {
'min': intervalBegin.valueOf(),
'max': intervalEnd.valueOf()
aggregations: {
intervals: {
date_histogram: {
field: 'date',
interval: interval,
min_doc_count: 0,
extended_bounds: {
min: intervalBegin.valueOf(),
max: intervalEnd.valueOf()
}
},
'aggregations': {
'total': {
'sum': {
'field': 'stat'
aggregations: {
total: {
sum: {
field: 'stat'
}
}
}
@ -549,11 +549,11 @@ Application.Controllers.controller('GraphsController', ['$scope', '$state', '$ro
// scale weeks on sunday as nvd3 supports only these weeks
if (interval === 'week') {
q.aggregations.subgroups.aggregations.intervals.date_histogram['offset'] = '-1d';
q.aggregations.subgroups.aggregations.intervals.date_histogram.offset = '-1d';
// scale days to UTC time
} else if (interval === 'day') {
const offset = moment().utcOffset();
q.aggregations.subgroups.aggregations.intervals.date_histogram['offset'] = (-offset) + 'm';
q.aggregations.subgroups.aggregations.intervals.date_histogram.offset = (-offset) + 'm';
}
return q;
};
@ -568,46 +568,46 @@ Application.Controllers.controller('GraphsController', ['$scope', '$state', '$ro
*/
var buildElasticAggregationsRankingQuery = function (groupKey, sortKey, intervalBegin, intervalEnd) {
const q = {
'query': {
'bool': {
'must': [
query: {
bool: {
must: [
{
'range': {
'date': {
'gte': intervalBegin.format(),
'lte': intervalEnd.format()
range: {
date: {
gte: intervalBegin.format(),
lte: intervalEnd.format()
}
}
},
{
'term': {
'type': 'booking'
term: {
type: 'booking'
}
}
]
}
},
'aggregations': {
'subgroups': {
'terms': {
'field': groupKey,
'size': 10,
'order': {
'total': 'desc'
aggregations: {
subgroups: {
terms: {
field: groupKey,
size: 10,
order: {
total: 'desc'
}
},
'aggregations': {
'top_events': {
'top_hits': {
'size': 1,
'sort': [
{ 'ca': 'desc' }
aggregations: {
top_events: {
top_hits: {
size: 1,
sort: [
{ ca: 'desc' }
]
}
},
'total': {
'sum': {
'field': 'stat'
total: {
sum: {
field: 'stat'
}
}
}
@ -716,12 +716,12 @@ Application.Controllers.controller('GraphsController', ['$scope', '$state', '$ro
values: []
}
];
for (let info of Array.from(data)) {
for (const info of Array.from(data)) {
if (info) {
newData[0].values.push({
'label': info.key,
'value': info.values[0].y,
'color': info.color
label: info.key,
value: info.values[0].y,
color: info.color
});
}
}

View File

@ -102,7 +102,7 @@ Application.Controllers.controller('SettingsController', ['$scope', '$rootScope'
`<div id="members">${_t('app.admin.settings.item_members')}</div>`,
`<div id="events">${_t('app.admin.settings.item_events')}</div>`
]
}
};
$scope.summernoteOptsHomePage.height = 400;
// codemirror editor
@ -110,15 +110,15 @@ Application.Controllers.controller('SettingsController', ['$scope', '$rootScope'
// Options for codemirror editor, used for custom css
$scope.codemirrorOpts = {
matchBrackets : true,
matchBrackets: true,
lineNumbers: true,
mode: 'sass'
}
};
// Show or hide advanced settings
$scope.advancedSettings = {
open: false
}
};
/**
* For use with 'ng-class', returns the CSS class name for the uploads previews.
@ -198,7 +198,7 @@ Application.Controllers.controller('SettingsController', ['$scope', '$rootScope'
} else {
$scope.privacyPolicy.version = null;
}
})
});
});
};
@ -212,7 +212,7 @@ Application.Controllers.controller('SettingsController', ['$scope', '$rootScope'
if ((content.custom_asset == null)) {
$scope.alerts = [];
return angular.forEach(content, function (v) {
angular.forEach(v, function(err) { growl.error(err); })
angular.forEach(v, function (err) { growl.error(err); });
});
} else {
growl.success(_t('app.admin.settings.file_successfully_updated'));
@ -251,7 +251,7 @@ Application.Controllers.controller('SettingsController', ['$scope', '$rootScope'
*/
$scope.addLoader = function (target) {
$scope.loader[target] = true;
}
};
/**
* Change the revision of the displayed privacy policy, from drafts history
@ -262,7 +262,7 @@ Application.Controllers.controller('SettingsController', ['$scope', '$rootScope'
return;
}
for (const draft of $scope.privacyDraftsHistory) {
if (draft.id == $scope.privacyPolicy.version) {
if (draft.id === $scope.privacyPolicy.version) {
$scope.privacyPolicy.bodyTemp = draft.content;
break;
}
@ -272,7 +272,7 @@ Application.Controllers.controller('SettingsController', ['$scope', '$rootScope'
/**
* Open a modal showing a sample of the collected data if FabAnalytics is enabled
*/
$scope.analyticsModal = function() {
$scope.analyticsModal = function () {
$uibModal.open({
templateUrl: '/admin/settings/analyticsModal.html',
controller: 'AnalyticsModalController',
@ -281,30 +281,30 @@ Application.Controllers.controller('SettingsController', ['$scope', '$rootScope'
analyticsData: ['FabAnalytics', function (FabAnalytics) { return FabAnalytics.data().$promise; }]
}
});
}
};
/**
* Reset the home page to its initial state (factory value)
*/
$scope.resetHomePage = function () {
dialogs.confirm({
resolve: {
object () {
return {
title: _t('app.admin.settings.confirmation_required'),
msg: _t('app.admin.settings.confirm_reset_home_page')
};
}
resolve: {
object () {
return {
title: _t('app.admin.settings.confirmation_required'),
msg: _t('app.admin.settings.confirm_reset_home_page')
};
}
}
, function () { // confirmed
Setting.reset({ name: 'home_content' }, function (data) {
$scope.homeContent.value = data.value;
growl.success(_t('app.admin.settings.home_content_reset'));
})
}
)
}
}
, function () { // confirmed
Setting.reset({ name: 'home_content' }, function (data) {
$scope.homeContent.value = data.value;
growl.success(_t('app.admin.settings.home_content_reset'));
});
}
);
};
/**
* Callback triggered when the codemirror editor is loaded into the DOM
@ -312,7 +312,7 @@ Application.Controllers.controller('SettingsController', ['$scope', '$rootScope'
*/
$scope.codemirrorLoaded = function (editor) {
$scope.codeMirrorEditor = editor;
}
};
/**
* Setup the feature-tour for the admin/settings page.
@ -336,7 +336,7 @@ Application.Controllers.controller('SettingsController', ['$scope', '$rootScope'
order: 1,
title: _t('app.admin.tour.settings.general.title'),
content: _t('app.admin.tour.settings.general.content'),
placement: 'bottom',
placement: 'bottom'
});
uitour.createStep({
selector: '.admin-settings .home-page-content h4',
@ -440,7 +440,7 @@ Application.Controllers.controller('SettingsController', ['$scope', '$rootScope'
if ($scope.allSettings.feature_tour_display !== 'manual' && $scope.currentUser.profile.tours.indexOf('settings') < 0) {
uitour.start();
}
}
};
/* PRIVATE SCOPE */
@ -491,7 +491,7 @@ Application.Controllers.controller('SettingsController', ['$scope', '$rootScope'
// refresh codemirror to display the fetched setting
$scope.$watch('advancedSettings.open', function (newValue) {
if (newValue) $scope.codeMirrorEditor.refresh();
})
});
// use the tours list, based on the selected value
$scope.$watch('allSettings.feature_tour_display', function (newValue, oldValue, scope) {
@ -513,7 +513,6 @@ Application.Controllers.controller('SettingsController', ['$scope', '$rootScope'
]);
/**
* Controller used in the invoice refunding modal window
*/
@ -550,13 +549,13 @@ Application.Controllers.controller('SavePolicyController', ['$scope', '$uibModal
* Controller used in the "what do we collect?" modal, about FabAnalytics
*/
Application.Controllers.controller('AnalyticsModalController', ['$scope', '$uibModalInstance', 'analyticsData',
function ($scope,$uibModalInstance, analyticsData) {
function ($scope, $uibModalInstance, analyticsData) {
// analytics data sample
$scope.data = analyticsData;
// callback to close the modal
$scope.close = function () {
$uibModalInstance.dismiss();
}
};
}
])
]);

View File

@ -81,7 +81,7 @@ class TrainingsController {
/**
* Controller used in the training creation page (admin)
*/
Application.Controllers.controller('NewTrainingController', [ '$scope', '$state', 'machinesPromise', 'CSRF',
Application.Controllers.controller('NewTrainingController', ['$scope', '$state', 'machinesPromise', 'CSRF',
function ($scope, $state, machinesPromise, CSRF) {
/* PUBLIC SCOPE */
@ -114,7 +114,7 @@ Application.Controllers.controller('NewTrainingController', [ '$scope', '$state'
/**
* Controller used in the training edition page (admin)
*/
Application.Controllers.controller('EditTrainingController', [ '$scope', '$state', '$stateParams', 'trainingPromise', 'machinesPromise', 'CSRF',
Application.Controllers.controller('EditTrainingController', ['$scope', '$state', '$stateParams', 'trainingPromise', 'machinesPromise', 'CSRF',
function ($scope, $state, $stateParams, trainingPromise, machinesPromise, CSRF) {
/* PUBLIC SCOPE */
@ -152,7 +152,6 @@ Application.Controllers.controller('EditTrainingController', [ '$scope', '$state
*/
Application.Controllers.controller('TrainingsAdminController', ['$scope', '$state', '$uibModal', 'Training', 'trainingsPromise', 'machinesPromise', '_t', 'growl', 'dialogs', 'Member', 'uiTourService', 'settingsPromise',
function ($scope, $state, $uibModal, Training, trainingsPromise, machinesPromise, _t, growl, dialogs, Member, uiTourService, settingsPromise) {
// list of trainings
$scope.trainings = trainingsPromise;
@ -262,7 +261,8 @@ Application.Controllers.controller('TrainingsAdminController', ['$scope', '$stat
*/
return $scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
}
] });
]
});
};
/**
@ -401,8 +401,7 @@ Application.Controllers.controller('TrainingsAdminController', ['$scope', '$stat
if (settingsPromise.feature_tour_display !== 'manual' && $scope.currentUser.profile.tours.indexOf('trainings') < 0) {
uitour.start();
}
}
};
/* PRIVATE SCOPE */
@ -411,7 +410,6 @@ Application.Controllers.controller('TrainingsAdminController', ['$scope', '$stat
*/
const initialize = function () {};
/**
* Group the trainings availabilities by trainings and by dates and return the resulting tree
* @param trainings {Array} $scope.trainings is expected here
@ -419,10 +417,10 @@ Application.Controllers.controller('TrainingsAdminController', ['$scope', '$stat
*/
const groupAvailabilities = function (trainings) {
const tree = {};
for (let training of Array.from(trainings)) {
for (const training of Array.from(trainings)) {
tree[training.name] = {};
tree[training.name].training = training;
for (let availability of Array.from(training.availabilities)) {
for (const availability of Array.from(training.availabilities)) {
const start = moment(availability.start_at);
// init the tree structure

View File

@ -184,7 +184,7 @@ class ProjectsController {
// reindex the remaining steps
return (function () {
const result = [];
for (let s of Array.from($scope.project.project_steps_attributes)) {
for (const s of Array.from($scope.project.project_steps_attributes)) {
if (s.step_nb > step.step_nb) {
result.push(s.step_nb -= 1);
} else {
@ -205,7 +205,7 @@ class ProjectsController {
*/
$scope.changeStepIndex = function (event, step, newIdx) {
if (event) { event.preventDefault(); }
for (let s of Array.from($scope.project.project_steps_attributes)) {
for (const s of Array.from($scope.project.project_steps_attributes)) {
if (s.step_nb === newIdx) {
s.step_nb = step.step_nb;
step.step_nb = newIdx;
@ -259,7 +259,7 @@ class ProjectsController {
return _t('app.shared.project.save_as_draft');
}
return _t('app.shared.buttons.save');
}
};
}
}
@ -277,7 +277,7 @@ Application.Controllers.controller('ProjectsController', ['$scope', '$state', 'P
/* PUBLIC SCOPE */
// Fab-manager's instance ID in the openLab network
$scope.openlabAppId = settingsPromise.openlab_app_id
$scope.openlabAppId = settingsPromise.openlab_app_id;
// Is openLab enabled on the instance?
$scope.openlab = {
@ -505,12 +505,12 @@ Application.Controllers.controller('EditProjectController', ['$rootScope', '$sco
if ($scope.project.author_id !== $rootScope.currentUser.id && $scope.project.user_ids.indexOf($rootScope.currentUser.id) === -1 && $scope.currentUser.role !== 'admin') {
$state.go('app.public.projects_show', { id: $scope.project.slug });
console.error('[EditProjectController::initialize] user is not allowed')
console.error('[EditProjectController::initialize] user is not allowed');
}
// Using the ProjectsController
return new ProjectsController($scope, $state, Project, Machine, Member, Component, Theme, Licence, $document, Diacritics, dialogs, allowedExtensions, _t);
}
};
// !!! MUST BE CALLED AT THE END of the controller
return initialize();

View File

@ -9,7 +9,7 @@
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
Application.Directives.directive('coupon', [ '$rootScope', 'Coupon', '_t', function ($rootScope, Coupon, _t) {
Application.Directives.directive('coupon', ['$rootScope', 'Coupon', '_t', function ($rootScope, Coupon, _t) {
return ({
restrict: 'E',
scope: {

View File

@ -11,7 +11,7 @@
*/
'use strict';
Application.Directives.directive('fileread', [ () =>
Application.Directives.directive('fileread', [() =>
({
scope: {
fileread: '='
@ -31,7 +31,7 @@ Application.Directives.directive('fileread', [ () =>
// image placeholder library.
//
// To use, simply define `bs-holder` on any element
Application.Directives.directive('bsHolder', [ () =>
Application.Directives.directive('bsHolder', [() =>
({
link (scope, element, attrs) {
Holder.addTheme('icon', { background: 'white', foreground: '#e9e9e9', size: 80, font: 'FontAwesome' })
@ -44,7 +44,7 @@ Application.Directives.directive('bsHolder', [ () =>
]);
Application.Directives.directive('match', [ () =>
Application.Directives.directive('match', [() =>
({
require: 'ngModel',
restrict: 'A',
@ -59,7 +59,7 @@ Application.Directives.directive('match', [ () =>
]);
Application.Directives.directive('publishProject', [ () =>
Application.Directives.directive('publishProject', [() =>
({
restrict: 'A',
link (scope, elem, attrs, ctrl) {
@ -94,7 +94,7 @@ Application.Directives.directive('disableAnimation', ['$animate', ($animate) =>
* Isolate a form's scope from its parent : no nested validation
* @see https://stackoverflow.com/a/37481846/1039377
*/
Application.Directives.directive('isolateForm', [ () =>
Application.Directives.directive('isolateForm', [() =>
({
restrict: 'A',
require: '?form',

View File

@ -8,7 +8,7 @@
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
Application.Directives.directive('fabUserAvatar', [ function () {
Application.Directives.directive('fabUserAvatar', [function () {
return ({
restrict: 'E',
scope: {

View File

@ -1,4 +1,4 @@
Application.Directives.directive('events', [ 'Event',
Application.Directives.directive('events', ['Event',
function (Event) {
return ({
restrict: 'E',
@ -12,15 +12,15 @@ Application.Directives.directive('events', [ 'Event',
* @param event {Object} single event from the $scope.upcomingEvents array
* @returns {boolean} false if the event runs on more that 1 day
*/
$scope.isOneDayEvent = function(event) {
$scope.isOneDayEvent = function (event) {
return moment(event.start_date).isSame(event.end_date, 'day');
}
};
// constructor
const initialize = function () {
Event.upcoming({ limit: 3 }, function (data) {
$scope.upcomingEvents = data;
})
});
};
// !!! MUST BE CALLED AT THE END of the directive

View File

@ -1,4 +1,4 @@
Application.Directives.directive('news', [ 'Setting',
Application.Directives.directive('news', ['Setting',
function (Setting) {
return ({
restrict: 'E',
@ -11,7 +11,7 @@ Application.Directives.directive('news', [ 'Setting',
const initialize = function () {
Setting.get({ name: 'home_blogpost' }, function (data) {
$scope.homeBlogpost = data.setting.value;
})
});
};
// !!! MUST BE CALLED AT THE END of the directive

View File

@ -1,4 +1,4 @@
Application.Directives.directive('projects', [ 'Project',
Application.Directives.directive('projects', ['Project',
function (Project) {
return ({
restrict: 'E',
@ -14,7 +14,7 @@ Application.Directives.directive('projects', [ 'Project',
const initialize = function () {
Project.lastPublished(function (data) {
$scope.lastProjects = data;
})
});
};
// !!! MUST BE CALLED AT THE END of the directive

View File

@ -19,20 +19,20 @@ Application.Directives.directive('twitter', ['Setting',
$scope.twitterName = data.setting.value;
if ($scope.twitterName) {
const configProfile = {
'profile': { 'screenName': $scope.twitterName },
'domId': 'twitter',
'maxTweets': 1,
'enableLinks': true,
'showUser': false,
'showTime': true,
'showImages': false,
'showRetweet': true,
'showInteraction': false,
'lang': Fablab.locale
profile: { screenName: $scope.twitterName },
domId: 'twitter',
maxTweets: 1,
enableLinks: true,
showUser: false,
showTime: true,
showImages: false,
showRetweet: true,
showInteraction: false,
lang: Fablab.locale
};
twitterFetcher.fetch(configProfile);
}
})
});
};
// !!! MUST BE CALLED AT THE END of the directive

View File

@ -1,4 +1,4 @@
Application.Directives.directive('members', [ 'Member',
Application.Directives.directive('members', ['Member',
function (Member) {
return ({
restrict: 'E',
@ -11,7 +11,7 @@ Application.Directives.directive('members', [ 'Member',
const initialize = function () {
Member.lastSubscribed({ limit: 4 }, function (data) {
$scope.lastMembers = data;
})
});
};
// !!! MUST BE CALLED AT THE END of the directive

View File

@ -1,4 +1,4 @@
Application.Directives.directive('postRender', [ '$timeout',
Application.Directives.directive('postRender', ['$timeout',
function ($timeout) {
return ({
restrict: 'A',

View File

@ -8,12 +8,12 @@
* which have a valid running subscription or not.
* Usage: <select-member [subscription="false|true"]></select-member>
*/
Application.Directives.directive('selectMember', [ 'Diacritics', 'Member', function (Diacritics, Member) {
Application.Directives.directive('selectMember', ['Diacritics', 'Member', function (Diacritics, Member) {
return ({
restrict: 'E',
templateUrl: '/shared/_member_select.html',
link (scope, element, attributes) {
return scope.autoCompleteName = function (nameLookup) {
scope.autoCompleteName = function (nameLookup) {
if (!nameLookup) {
return;
}
@ -22,7 +22,7 @@ Application.Directives.directive('selectMember', [ 'Diacritics', 'Member', funct
const q = { query: asciiName };
if (attributes.subscription) {
q['subscription'] = attributes.subscription;
q.subscription = attributes.subscription;
}
Member.search(q, function (users) {

View File

@ -27,7 +27,7 @@ Application.Directives.directive('selectSetting', ['Setting', 'growl', '_t',
* @param setting {{value:*, name:string}} note that the value will be stringified
*/
$scope.save = function (setting) {
let { value } = setting;
const { value } = setting;
Setting.update(
{ name: setting.name },

View File

@ -38,7 +38,7 @@ Application.Directives.directive('textSetting', ['Setting', 'growl', '_t',
* @param setting {{value:*, name:string}} note that the value will be stringified
*/
$scope.save = function (setting) {
let { value } = setting;
const { value } = setting;
Setting.update(
{ name: setting.name },

View File

@ -9,7 +9,7 @@
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
Application.Directives.directive('socialLink', [ function () {
Application.Directives.directive('socialLink', [function () {
return ({
restrict: 'E',
scope: {
@ -19,10 +19,10 @@ Application.Directives.directive('socialLink', [ function () {
templateUrl: '/shared/_social_link.html',
link (scope, element, attributes) {
if (scope.network === 'dailymotion') {
scope.image = "social/dailymotion.png";
scope.image = 'social/dailymotion.png';
return scope.altText = 'd';
} else if (scope.network === 'echosciences') {
scope.image = "social/echosciences.png";
scope.image = 'social/echosciences.png';
return scope.altText = 'E)';
} else {
if (scope.network === 'website') {

View File

@ -12,7 +12,7 @@
*/
'use strict';
Application.Directives.directive('url', [ function () {
Application.Directives.directive('url', [function () {
const URL_REGEXP = /^(https?:\/\/)([\da-z\.-]+)\.([-a-z0-9\.]{2,30})([\/\w \.-]*)*\/?$/;
return {
require: 'ngModel',
@ -33,7 +33,7 @@ Application.Directives.directive('url', [ function () {
}
]);
Application.Directives.directive('endpoint', [ function () {
Application.Directives.directive('endpoint', [function () {
const ENDPOINT_REGEXP = /^\/?([-._~:?#\[\]@!$&'()*+,;=%\w]+\/?)*$/;
return {
require: 'ngModel',

Some files were not shown because too many files have changed in this diff Show More