1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-03-23 14:19:52 +01:00

refactor API services to use only static methods

Also: separate reserve-button component
This commit is contained in:
Sylvain 2021-06-17 17:08:22 +02:00
parent 67ed329dd7
commit bff5415a1e
28 changed files with 243 additions and 166 deletions

View File

@ -1,17 +1,11 @@
import apiClient from './clients/api-client';
import { AxiosResponse } from 'axios';
import { CustomAsset, CustomAssetName } from '../models/custom-asset';
import wrapPromise, { IWrapPromise } from '../lib/wrap-promise';
export default class CustomAssetAPI {
async get (name: CustomAssetName): Promise<CustomAsset> {
static async get (name: CustomAssetName): Promise<CustomAsset> {
const res: AxiosResponse = await apiClient.get(`/api/custom_assets/${name}`);
return res?.data?.custom_asset;
}
static get (name: CustomAssetName): IWrapPromise<CustomAsset> {
const api = new CustomAssetAPI();
return wrapPromise(api.get(name));
}
}

View File

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

View File

@ -7,5 +7,10 @@ export default class MachineAPI {
const res: AxiosResponse<Array<Machine>> = await apiClient.get(`/api/machines`);
return res?.data;
}
static async get (id: number): Promise<Machine> {
const res: AxiosResponse<Machine> = await apiClient.get(`/api/machines/${id}`);
return res?.data;
}
}

View File

@ -8,32 +8,32 @@ import {
} from '../models/payment-schedule';
export default class PaymentScheduleAPI {
async list (query: PaymentScheduleIndexRequest): Promise<Array<PaymentSchedule>> {
static async list (query: PaymentScheduleIndexRequest): Promise<Array<PaymentSchedule>> {
const res: AxiosResponse = await apiClient.post(`/api/payment_schedules/list`, query);
return res?.data;
}
async index (query: PaymentScheduleIndexRequest): Promise<Array<PaymentSchedule>> {
static async index (query: PaymentScheduleIndexRequest): Promise<Array<PaymentSchedule>> {
const res: AxiosResponse = await apiClient.get(`/api/payment_schedules?page=${query.query.page}&size=${query.query.size}`);
return res?.data;
}
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;
}
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;
}
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;
}
async cancel (paymentScheduleId: number): Promise<CancelScheduleResponse> {
static async cancel (paymentScheduleId: number): Promise<CancelScheduleResponse> {
const res: AxiosResponse = await apiClient.put(`/api/payment_schedules/${paymentScheduleId}/cancel`);
return res?.data;
}

View File

@ -1,15 +1,14 @@
import apiClient from './clients/api-client';
import { AxiosResponse } from 'axios';
import { Setting, SettingBulkResult, SettingError, SettingName } from '../models/setting';
import wrapPromise, { IWrapPromise } from '../lib/wrap-promise';
export default class SettingAPI {
async get (name: SettingName): Promise<Setting> {
static async get (name: SettingName): Promise<Setting> {
const res: AxiosResponse<{setting: Setting}> = await apiClient.get(`/api/settings/${name}`);
return res?.data?.setting;
}
async query (names: Array<SettingName>): Promise<Map<SettingName, any>> {
static async query (names: Array<SettingName>): Promise<Map<SettingName, any>> {
const params = new URLSearchParams();
params.append('names', `['${names.join("','")}']`);
@ -17,37 +16,22 @@ export default class SettingAPI {
return SettingAPI.toSettingsMap(names, res?.data);
}
async update (name: SettingName, value: any): Promise<Setting> {
static async update (name: SettingName, value: any): Promise<Setting> {
const res: AxiosResponse = await apiClient.patch(`/api/settings/${name}`, { setting: { value } });
if (res.status === 304) { return { name, value }; }
return res?.data?.setting;
}
async bulkUpdate (settings: Map<SettingName, any>, transactional: boolean = false): Promise<Map<SettingName, SettingBulkResult>> {
static async bulkUpdate (settings: Map<SettingName, any>, transactional: boolean = 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);
}
async isPresent (name: SettingName): Promise<boolean> {
static async isPresent (name: SettingName): Promise<boolean> {
const res: AxiosResponse = await apiClient.get(`/api/settings/is_present/${name}`);
return res?.data?.isPresent;
}
static get (name: SettingName): IWrapPromise<Setting> {
const api = new SettingAPI();
return wrapPromise(api.get(name));
}
static query (names: Array<SettingName>): IWrapPromise<Map<SettingName, any>> {
const api = new SettingAPI();
return wrapPromise(api.query(names));
}
static isPresent (name: SettingName): IWrapPromise<boolean> {
const api = new SettingAPI();
return wrapPromise(api.isPresent(name));
}
private static toSettingsMap(names: Array<SettingName>, data: Object): Map<SettingName, any> {
const map = new Map();
names.forEach(name => {

View File

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

View File

@ -1,6 +1,5 @@
import apiClient from './clients/api-client';
import { AxiosResponse } from 'axios';
import wrapPromise, { IWrapPromise } from '../lib/wrap-promise';
import { Wallet } from '../models/wallet';
export default class WalletAPI {

View File

@ -1,9 +1,9 @@
import React, { ReactNode, BaseSyntheticEvent, useEffect } from 'react';
import React, { ReactNode, BaseSyntheticEvent, useEffect, useState } from 'react';
import Modal from 'react-modal';
import { useTranslation } from 'react-i18next';
import { Loader } from './loader';
import CustomAssetAPI from '../../api/custom-asset';
import { CustomAssetName } from '../../models/custom-asset';
import { CustomAsset, CustomAssetName } from '../../models/custom-asset';
import { FabButton } from './fab-button';
Modal.setAppElement('body');
@ -28,24 +28,25 @@ interface FabModalProps {
onCreation?: () => void,
}
// initial request to the API
const blackLogoFile = CustomAssetAPI.get(CustomAssetName.LogoBlackFile);
/**
* This component is a template for a modal dialog that wraps the application style
*/
export const FabModal: React.FC<FabModalProps> = ({ title, isOpen, toggleModal, children, confirmButton, className, width = 'sm', closeButton, customFooter, onConfirm, preventConfirm, onCreation }) => {
const { t } = useTranslation('shared');
const [blackLogo, setBlackLogo] = useState<CustomAsset>(null);
// initial request to the API to get the theme's logo, for back backgrounds
useEffect(() => {
CustomAssetAPI.get(CustomAssetName.LogoBlackFile).then(data => setBlackLogo(data));
}, []);
useEffect(() => {
if (typeof onCreation === 'function' && isOpen) {
onCreation();
}
}, [isOpen]);
// the theme's logo, for back backgrounds
const blackLogo = blackLogoFile.read();
/**
* Check if the confirm button should be present
*/
@ -74,9 +75,9 @@ export const FabModal: React.FC<FabModalProps> = ({ title, isOpen, toggleModal,
onRequestClose={toggleModal}>
<div className="fab-modal-header">
<Loader>
<img src={blackLogo.custom_asset_file_attributes.attachment_url}
alt={blackLogo.custom_asset_file_attributes.attachment}
className="modal-logo" />
{blackLogo && <img src={blackLogo.custom_asset_file_attributes.attachment_url}
alt={blackLogo.custom_asset_file_attributes.attachment}
className="modal-logo" />}
</Loader>
<h1>{ title }</h1>
</div>

View File

@ -30,7 +30,7 @@ const EventThemes: React.FC<EventThemesProps> = ({ event, onChange }) => {
const [themes, setThemes] = useState<Array<EventTheme>>([]);
useEffect(() => {
new EventThemeAPI().index().then(data => setThemes(data));
EventThemeAPI.index().then(data => setThemes(data));
}, []);
/**

View File

@ -1,29 +1,35 @@
import React, { ReactNode } from 'react';
import React, { ReactNode, useState } from 'react';
import { Machine } from '../../models/machine';
import { useTranslation } from 'react-i18next';
import { Loader } from '../base/loader';
import { ReserveButton } from './reserve-button';
import { User } from '../../models/user';
interface MachineCardProps {
user?: User,
machine: Machine,
onShowMachine: (machine: Machine) => void,
onReserveMachine: (machine: Machine) => void,
onLoginRequested: () => Promise<User>,
onError: (message: string) => void,
}
/**
* This component is a box showing the picture of the given machine and two buttons: one to start the reservation process
* and another to redirect the user to the machine description page.
*/
const MachineCardComponent: React.FC<MachineCardProps> = ({ machine, onShowMachine, onReserveMachine }) => {
const MachineCardComponent: React.FC<MachineCardProps> = ({ user, machine, onShowMachine, onReserveMachine, onError, onLoginRequested }) => {
const { t } = useTranslation('public');
// shall we display a loader to prevent double-clicking, while the machine details are loading?
const [loading, setLoading] = useState<boolean>(false);
/**
* Callback triggered when the user clicks on the 'reserve' button.
* We handle the training verification process here, before redirecting the user to the reservation calendar.
* Callback triggered when the user clicks on the 'reserve' button and has passed all the verifications
*/
const handleReserveMachine = (): void => {
onReserveMachine(machine);
}
/**
* Callback triggered when the user clicks on the 'view' button
*/
@ -49,16 +55,23 @@ const MachineCardComponent: React.FC<MachineCardProps> = ({ machine, onShowMachi
}
return (
<div className="machine-card">
<div className={`machine-card ${loading ? 'loading' : ''}`}>
{machinePicture()}
<div className="machine-name">
{machine.name}
</div>
<div className="machine-actions">
{!machine.disabled && <button onClick={handleReserveMachine} className="reserve-button">
{!machine.disabled && <ReserveButton currentUser={user}
machineId={machine.id}
onLoadingStart={() => setLoading(true)}
onLoadingEnd={() => setLoading(false)}
onError={onError}
onReserveMachine={handleReserveMachine}
onLoginRequested={onLoginRequested}
className="reserve-button">
<i className="fas fa-bookmark" />
{t('app.public.machine_card.book')}
</button>}
</ReserveButton>}
<button onClick={handleShowMachine} className={`show-button ${machine.disabled ? 'single-button': ''}`}>
<i className="fas fa-eye" />
{t('app.public.machine_card.consult')}
@ -69,10 +82,10 @@ const MachineCardComponent: React.FC<MachineCardProps> = ({ machine, onShowMachi
}
export const MachineCard: React.FC<MachineCardProps> = ({ machine, onShowMachine, onReserveMachine }) => {
export const MachineCard: React.FC<MachineCardProps> = ({ user, machine, onShowMachine, onReserveMachine, onError, onLoginRequested }) => {
return (
<Loader>
<MachineCardComponent machine={machine} onShowMachine={onShowMachine} onReserveMachine={onReserveMachine} />
<MachineCardComponent user={user} machine={machine} onShowMachine={onShowMachine} onReserveMachine={onReserveMachine} onError={onError} onLoginRequested={onLoginRequested} />
</Loader>
);
}

View File

@ -6,19 +6,22 @@ import { Loader } from '../base/loader';
import MachineAPI from '../../api/machine';
import { MachineCard } from './machine-card';
import { MachinesFilters } from './machines-filters';
import { User } from '../../models/user';
declare var Application: IApplication;
interface MachinesListProps {
user?: User,
onError: (message: string) => void,
onShowMachine: (machine: Machine) => void,
onReserveMachine: (machine: Machine) => void,
onLoginRequested: () => Promise<User>,
}
/**
* This component shows a list of all machines and allows filtering on that list.
*/
const MachinesList: React.FC<MachinesListProps> = ({ onError, onShowMachine, onReserveMachine }) => {
const MachinesList: React.FC<MachinesListProps> = ({ onError, onShowMachine, onReserveMachine, onLoginRequested, user }) => {
// shown machines
const [machines, setMachines] = useState<Array<Machine>>(null);
// we keep the full list of machines, for filtering
@ -52,7 +55,13 @@ const MachinesList: React.FC<MachinesListProps> = ({ onError, onShowMachine, onR
<MachinesFilters onStatusSelected={handleFilterByStatus} />
<div className="all-machines">
{machines && machines.map(machine => {
return <MachineCard key={machine.id} machine={machine} onShowMachine={onShowMachine} onReserveMachine={onReserveMachine} />
return <MachineCard key={machine.id}
user={user}
machine={machine}
onShowMachine={onShowMachine}
onReserveMachine={onReserveMachine}
onError={onError}
onLoginRequested={onLoginRequested} />
})}
</div>
</div>
@ -60,12 +69,12 @@ const MachinesList: React.FC<MachinesListProps> = ({ onError, onShowMachine, onR
}
const MachinesListWrapper: React.FC<MachinesListProps> = ({ onError, onShowMachine, onReserveMachine }) => {
const MachinesListWrapper: React.FC<MachinesListProps> = ({ user, onError, onShowMachine, onReserveMachine, onLoginRequested }) => {
return (
<Loader>
<MachinesList onError={onError} onShowMachine={onShowMachine} onReserveMachine={onReserveMachine} />
<MachinesList user={user} onError={onError} onShowMachine={onShowMachine} onReserveMachine={onReserveMachine} onLoginRequested={onLoginRequested} />
</Loader>
);
}
Application.Components.component('machinesList', react2angular(MachinesListWrapper, ['onError', 'onShowMachine', 'onReserveMachine']));
Application.Components.component('machinesList', react2angular(MachinesListWrapper, ['user', 'onError', 'onShowMachine', 'onReserveMachine', 'onLoginRequested']));

View File

@ -0,0 +1,74 @@
import React, { useState } from 'react';
import MachineAPI from '../../api/machine';
import { Machine } from '../../models/machine';
import { User } from '../../models/user';
interface ReserveButtonProps {
currentUser?: User,
machineId: number,
onLoadingStart?: () => void,
onLoadingEnd?: () => void,
onError: (message: string) => void,
onReserveMachine: (machineId: number) => void,
onLoginRequested: () => Promise<User>,
className?: string
}
/**
* Button component that makes the training verification before redirecting the user to the reservation calendar
*/
export const ReserveButton: React.FC<ReserveButtonProps> = ({ currentUser, machineId, onLoginRequested, onLoadingStart, onLoadingEnd, onError, onReserveMachine, className, children }) => {
const [pendingTraining, setPendingTraining] = useState<boolean>(false);
/**
* Callback triggered when the user clicks on the 'reserve' button.
* We load the full machine data, then we check if the user has passed the training for it (if it's needed)
*/
const handleClick = (user?: User): void => {
if (onLoadingStart) onLoadingStart();
MachineAPI.get(machineId)
.then(data => {
checkTraining(data, user);
if (onLoadingEnd) onLoadingEnd();
})
.catch(error => {
onError(error);
if (onLoadingEnd) onLoadingEnd();
});
}
/**
* Check that the current user has passed the required training before allowing him to book
*/
const checkTraining = (machine: Machine, user?: User): void => {
// if there's no user currently logged, trigger the logging process
if (!user) {
onLoginRequested()
.then(user => handleClick(user))
.catch(error => onError(error));
return;
}
// if the currently logged user has completed the training for this machine, or this machine does not require
// a prior training, just let him booking
if (machine.current_user_is_trained || machine.trainings.length === 0) {
return onReserveMachine(machineId);
}
// if a user is authenticated and have booked a training for this machine, tell him that he must wait
// for an admin to validate the training before he can book the reservation
if (machine.current_user_next_training_reservation) {
return setPendingTraining(true);
}
}
return (
<span>
<button onClick={() => handleClick(currentUser)} className={className}>
{children}
</button>
</span>
);
}

View File

@ -45,8 +45,7 @@ const PaymentSchedulesDashboard: React.FC<PaymentSchedulesDashboardProps> = ({ c
const handleLoadMore = (): void => {
setPageNumber(pageNumber + 1);
const api = new PaymentScheduleAPI();
api.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));
@ -56,8 +55,7 @@ const PaymentSchedulesDashboard: React.FC<PaymentSchedulesDashboardProps> = ({ c
* Reload from te API all the currently displayed payment schedules
*/
const handleRefreshList = (): void => {
const api = new PaymentScheduleAPI();
api.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);

View File

@ -53,8 +53,7 @@ const PaymentSchedulesList: React.FC<PaymentSchedulesListProps> = ({ currentUser
setCustomerFilter(customer);
setDateFilter(date);
const api = new PaymentScheduleAPI();
api.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));
};
@ -65,8 +64,7 @@ const PaymentSchedulesList: React.FC<PaymentSchedulesListProps> = ({ currentUser
const handleLoadMore = (): void => {
setPageNumber(pageNumber + 1);
const api = new PaymentScheduleAPI();
api.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));
@ -76,8 +74,7 @@ const PaymentSchedulesList: React.FC<PaymentSchedulesListProps> = ({ currentUser
* Reload from te API all the currently displayed payment schedules
*/
const handleRefreshList = (): void => {
const api = new PaymentScheduleAPI();
api.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);

View File

@ -222,8 +222,7 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
* After the user has confirmed that he wants to cash the check, update the API, refresh the list and close the modal.
*/
const onCheckCashingConfirmed = (): void => {
const api = new PaymentScheduleAPI();
api.cashCheck(tempDeadline.id).then((res) => {
PaymentScheduleAPI.cashCheck(tempDeadline.id).then((res) => {
if (res.state === PaymentScheduleItemState.Paid) {
refreshSchedulesTable();
toggleConfirmCashingModal();
@ -267,8 +266,7 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
*/
const afterAction = (): void => {
toggleConfirmActionButton();
const api = new PaymentScheduleAPI();
api.refreshItem(tempDeadline.id).then(() => {
PaymentScheduleAPI.refreshItem(tempDeadline.id).then(() => {
refreshSchedulesTable();
toggleResolveActionModal();
});
@ -304,8 +302,7 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
*/
const handleCardUpdateSuccess = (): void => {
if (tempDeadline) {
const api = new PaymentScheduleAPI();
api.payItem(tempDeadline.id).then(() => {
PaymentScheduleAPI.payItem(tempDeadline.id).then(() => {
refreshSchedulesTable();
onCardUpdateSuccess();
toggleUpdateCardModal();
@ -347,8 +344,7 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
* When the user has confirmed the cancellation, we transfer the request to the API
*/
const onCancelSubscriptionConfirmed = (): void => {
const api = new PaymentScheduleAPI();
api.cancel(tempSchedule.id).then(() => {
PaymentScheduleAPI.cancel(tempSchedule.id).then(() => {
refreshSchedulesTable();
toggleCancelSubscriptionModal();
});

View File

@ -4,7 +4,7 @@ import WalletLib from '../../lib/wallet';
import { WalletInfo } from '../wallet-info';
import { FabModal, ModalSize } from '../base/fab-modal';
import { HtmlTranslate } from '../base/html-translate';
import { CustomAssetName } from '../../models/custom-asset';
import { CustomAsset, CustomAssetName } from '../../models/custom-asset';
import { IFablab } from '../../models/fablab';
import { ShoppingCart } from '../../models/payment';
import { PaymentSchedule } from '../../models/payment-schedule';
@ -48,10 +48,6 @@ interface AbstractPaymentModalProps {
formClassName?: string,
}
// initial request to the API
const cgvFile = CustomAssetAPI.get(CustomAssetName.CgvFile);
/**
* This component is an abstract modal that must be extended by each payment gateway to include its payment form.
*
@ -75,17 +71,18 @@ export const AbstractPaymentModal: React.FC<AbstractPaymentModalProps> = ({ isOp
const [tos, setTos] = useState<boolean>(false);
// currently active payment gateway
const [gateway, setGateway] = useState<string>(null);
// the sales conditions
const [cgv, setCgv] = useState<CustomAsset>(null);
const { t } = useTranslation('shared');
const cgv = cgvFile.read();
/**
* When the component is loaded first, get the name of the currently active payment modal
*/
useEffect(() => {
const api = new SettingAPI();
api.get(SettingName.PaymentGateway).then((setting) => {
CustomAssetAPI.get(CustomAssetName.CgvFile).then(asset => setCgv(asset));
SettingAPI.get(SettingName.PaymentGateway).then((setting) => {
// we capitalize the first letter of the name
setGateway(setting.value.replace(/^\w/, (c) => c.toUpperCase()));
})

View File

@ -1,4 +1,4 @@
import React, { ReactElement } from 'react';
import React, { ReactElement, useEffect, useState } from 'react';
import { react2angular } from 'react2angular';
import { Loader } from '../base/loader';
import { StripeModal } from './stripe/stripe-modal';
@ -7,7 +7,7 @@ import { IApplication } from '../../models/application';
import { ShoppingCart } from '../../models/payment';
import { User } from '../../models/user';
import { PaymentSchedule } from '../../models/payment-schedule';
import { SettingName } from '../../models/setting';
import { Setting, SettingName } from '../../models/setting';
import { Invoice } from '../../models/invoice';
import SettingAPI from '../../api/setting';
import { useTranslation } from 'react-i18next';
@ -25,16 +25,20 @@ interface PaymentModalProps {
customer: User
}
// initial request to the API
const paymentGateway = SettingAPI.get(SettingName.PaymentGateway);
/**
* 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 PaymentModal: React.FC<PaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, currentUser, schedule , cart, customer }) => {
const { t } = useTranslation('shared');
const gateway = paymentGateway.read();
const [gateway, setGateway] = useState<Setting>(null);
useEffect(() => {
SettingAPI.get(SettingName.PaymentGateway)
.then(setting => setGateway(setting))
.catch(error => onError(error));
}, []);
/**
* Render the Stripe payment modal
@ -65,6 +69,8 @@ const PaymentModal: React.FC<PaymentModalProps> = ({ isOpen, toggleModal, afterS
/**
* Determine which gateway is enabled and return the appropriate payment modal
*/
if (gateway === null || !isOpen) return <div/>;
switch (gateway.value) {
case 'stripe':
return renderStripeModal();

View File

@ -30,8 +30,7 @@ export const PayzenForm: React.FC<PayzenFormProps> = ({ onSubmit, onSuccess, onE
const [loadingClass, setLoadingClass] = useState<'hidden' | 'loader' | 'loader-overlay'>('loader');
useEffect(() => {
const api = new SettingAPI();
api.query([SettingName.PayZenEndpoint, SettingName.PayZenPublicKey]).then(settings => {
SettingAPI.query([SettingName.PayZenEndpoint, SettingName.PayZenPublicKey]).then(settings => {
createToken().then(formToken => {
// Load the remote library
KRGlue.loadLibrary(settings.get(SettingName.PayZenEndpoint), settings.get(SettingName.PayZenPublicKey))

View File

@ -46,8 +46,7 @@ const PayZenKeysFormComponent: React.FC<PayZenKeysFormProps> = ({ onValidKeys, o
* When the component loads for the first time, initialize the keys with the values fetched from the API (if any)
*/
useEffect(() => {
const api = new SettingAPI();
api.query(payZenSettings).then(payZenKeys => {
SettingAPI.query(payZenSettings).then(payZenKeys => {
updateSettings(new Map(payZenKeys));
}).catch(error => console.error(error));
}, []);

View File

@ -54,10 +54,9 @@ export const PayzenSettings: React.FC<PayzenSettingsProps> = ({ onEditKeys, onCu
* For the private settings, we initialize them with the placeholder value, if the setting is set.
*/
useEffect(() => {
const api = new SettingAPI();
api.query(payZenPublicSettings.concat(payZenOtherSettings)).then(payZenKeys => {
api.isPresent(SettingName.PayZenPassword).then(pzPassword => {
api.isPresent(SettingName.PayZenHmacKey).then(pzHmac => {
SettingAPI.query(payZenPublicSettings.concat(payZenOtherSettings)).then(payZenKeys => {
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 : '');
@ -94,8 +93,7 @@ export const PayzenSettings: React.FC<PayzenSettingsProps> = ({ onEditKeys, onCu
* This will update the setting on the server.
*/
const saveCurrency = (): void => {
const api = new SettingAPI();
api.update(SettingName.PayZenCurrency, settings.get(SettingName.PayZenCurrency)).then(result => {
SettingAPI.update(SettingName.PayZenCurrency, settings.get(SettingName.PayZenCurrency)).then(result => {
setError('');
updateSettings(draft => draft.set(SettingName.PayZenCurrency, result.value));
onCurrencyUpdateSuccess(result.value);

View File

@ -4,9 +4,6 @@ import { loadStripe } from "@stripe/stripe-js";
import { SettingName } from '../../../models/setting';
import SettingAPI from '../../../api/setting';
// initial request to the API
const stripePublicKey = SettingAPI.get(SettingName.StripePublicKey);
/**
* This component initializes the stripe's Elements tag with the API key
*/
@ -17,9 +14,10 @@ export const StripeElements: React.FC = memo(({ children }) => {
* When this component is mounted, we initialize the <Elements> tag with the Stripe's public key
*/
useEffect(() => {
const key = stripePublicKey.read();
const promise = loadStripe(key.value);
setStripe(promise);
SettingAPI.get(SettingName.StripePublicKey).then(key => {
const promise = loadStripe(key.value);
setStripe(promise);
});
}, [])
return (

View File

@ -43,8 +43,7 @@ const StripeKeysFormComponent: React.FC<StripeKeysFormProps> = ({ onValidKeys, o
useEffect(() => {
mounted.current = true;
const api = new SettingAPI();
api.query([SettingName.StripePublicKey, SettingName.StripeSecretKey]).then(stripeKeys => {
SettingAPI.query([SettingName.StripePublicKey, SettingName.StripeSecretKey]).then(stripeKeys => {
setPublicKey(stripeKeys.get(SettingName.StripePublicKey));
setSecretKey(stripeKeys.get(SettingName.StripeSecretKey));
}).catch(error => console.error(error));

View File

@ -27,9 +27,6 @@ interface SelectGatewayModalModalProps {
onSuccess: (results: Map<SettingName, SettingBulkResult>) => void,
}
// initial request to the API
const paymentGateway = SettingAPI.get(SettingName.PaymentGateway);
const SelectGatewayModal: React.FC<SelectGatewayModalModalProps> = ({ isOpen, toggleModal, onError, onSuccess }) => {
const { t } = useTranslation('admin');
@ -37,9 +34,11 @@ const SelectGatewayModal: React.FC<SelectGatewayModalModalProps> = ({ isOpen, to
const [selectedGateway, setSelectedGateway] = useState<string>('');
const [gatewayConfig, setGatewayConfig] = useState<Map<SettingName, string>>(new Map());
// request the configured gateway to the API
useEffect(() => {
const gateway = paymentGateway.read();
setSelectedGateway(gateway.value ? gateway.value : '');
SettingAPI.get(SettingName.PaymentGateway).then(gateway => {
setSelectedGateway(gateway.value ? gateway.value : '');
})
}, []);
/**
@ -101,8 +100,7 @@ const SelectGatewayModal: React.FC<SelectGatewayModalModalProps> = ({ isOpen, to
const settings = new Map<SettingName, string>(gatewayConfig);
settings.set(SettingName.PaymentGateway, selectedGateway);
const api = new SettingAPI();
api.bulkUpdate(settings, true).then(result => {
SettingAPI.bulkUpdate(settings, true).then(result => {
const errorResults = Array.from(result.values()).filter(item => !item.status);
if (errorResults.length > 0) {
onError(errorResults.map(item => item.error[0]).join(' '));

View File

@ -180,21 +180,38 @@ const _reserveMachine = function (machine, e) {
/**
* Controller used in the public listing page, allowing everyone to see the list of machines
*/
Application.Controllers.controller('MachinesController', ['$scope', '$state', '_t', 'AuthService', 'Machine', '$uibModal', 'machinesPromise', 'settingsPromise', 'Member', 'uiTourService',
function ($scope, $state, _t, AuthService, Machine, $uibModal, machinesPromise, settingsPromise, Member, uiTourService) {
Application.Controllers.controller('MachinesController', ['$scope', '$state', '_t', 'AuthService', 'Machine', '$uibModal', 'machinesPromise', 'settingsPromise', 'Member', 'uiTourService', 'growl',
function ($scope, $state, _t, AuthService, Machine, $uibModal, machinesPromise, settingsPromise, Member, uiTourService, growl) {
/* PUBLIC SCOPE */
// Retrieve the list of machines
$scope.machines = machinesPromise;
/**
* Redirect the user to the machine details page
*/
* Redirect the user to the machine details page
*/
$scope.showMachine = function (machine) { $state.go('app.public.machines_show', { id: machine.slug }); };
/**
* Callback to book a reservation for the current machine
*/
* Shows an error message forwarded from a child component
*/
$scope.onError = function (message) {
growl.error(message);
}
/**
* Open the modal dialog to log the user and resolves the returned promise when the logging process
* was successfully completed.
*/
$scope.onLoginRequest = function (e) {
return new Promise((resolve, _reject) => {
$scope.login(e, resolve);
});
}
/**
* Callback to book a reservation for the current machine
*/
$scope.reserveMachine = _reserveMachine.bind({
$scope,
$state,

View File

@ -1,37 +0,0 @@
/**
* This function wraps a Promise to make it compatible with react Suspense
*/
export interface IWrapPromise<T> {
read: () => T
}
function wrapPromise(promise: Promise<any>): IWrapPromise<any> {
let status: string = 'pending';
let response: any;
const suspender: Promise<any> = promise.then(
(res) => {
status = 'success'
response = res
},
(err) => {
status = 'error'
response = err
},
);
const read = (): any => {
switch (status) {
case 'pending':
throw suspender
case 'error':
throw response
default:
return response
}
};
return { read };
}
export default wrapPromise;

View File

@ -5,6 +5,36 @@
margin: 0 15px 30px;
width: 30%;
min-width: 263px;
position: relative;
&.loading::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(29, 29, 29, 0.5);
border-radius: 6px;
margin: -1px;
}
&.loading::after {
content: '\f1ce';
font-family: 'Font Awesome 5 Free';
text-align: center;
font-weight: 900;
position: absolute;
left: 41%;
font-size: 4em;
top: 35%;
color: white;
animation: spin 2s linear infinite;
}
@keyframes spin {
100% { transform: rotate(360deg);}
}
@media screen and (max-width: 1219px) {
width: 45%;

View File

@ -41,8 +41,11 @@
ui-tour-scroll-parent-id="content-main"
post-render="setupMachinesTour">
<machines-list on-show-machine="showMachine"
on-reserve-machine="reserveMachine">
<machines-list user="currentUser"
on-error="onError"
on-show-machine="showMachine"
on-reserve-machine="reserveMachine"
on-login-requested="onLoginRequest">
</machines-list>
</section>