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:
commit
0b33297f9a
@ -1,3 +1,4 @@
|
||||
node_modules/**
|
||||
vendor/**
|
||||
public/**
|
||||
|
||||
|
39
.eslintrc
39
.eslintrc
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
14
CHANGELOG.md
14
CHANGELOG.md
@ -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
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
||||
|
@ -8,4 +8,3 @@ export default class CustomAssetAPI {
|
||||
return res?.data?.custom_asset;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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('&');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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('&');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -23,4 +23,3 @@ export default class PlanCategoryAPI {
|
||||
return res?.data;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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('&');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -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('&');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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('&');
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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', []);
|
||||
|
@ -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']));
|
||||
|
@ -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>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
@ -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' };
|
||||
|
||||
|
@ -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 };
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -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) }} />
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -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']));
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
@ -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']));
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
@ -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']));
|
||||
|
@ -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']));
|
||||
|
@ -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']));
|
||||
|
@ -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']));
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -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']));
|
||||
|
@ -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',
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -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']));
|
||||
|
@ -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']));
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -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} />
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -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']));
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -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>;
|
||||
}
|
||||
};
|
||||
|
@ -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';
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -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} />
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
};
|
||||
|
@ -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']));
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
@ -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']));
|
||||
|
@ -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']));
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -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']));
|
||||
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -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']));
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -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']));
|
||||
|
@ -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 = [];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
])
|
||||
]);
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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: {
|
||||
|
@ -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',
|
||||
|
@ -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: {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -1,4 +1,4 @@
|
||||
Application.Directives.directive('postRender', [ '$timeout',
|
||||
Application.Directives.directive('postRender', ['$timeout',
|
||||
function ($timeout) {
|
||||
return ({
|
||||
restrict: 'A',
|
||||
|
@ -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) {
|
||||
|
@ -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 },
|
||||
|
@ -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 },
|
||||
|
@ -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') {
|
||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user