1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2024-11-28 09:24:24 +01:00

linted TSX files

This commit is contained in:
Sylvain 2021-07-01 12:34:10 +02:00
parent 54c933523d
commit f6889fbfda
60 changed files with 935 additions and 994 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,7 +10,7 @@ import WalletLib from '../lib/wallet';
import { ShoppingCart } from '../models/payment';
import FormatLib from '../lib/format';
declare var Application: IApplication;
declare const Application: IApplication;
interface WalletInfoProps {
cart: ShoppingCart,
@ -39,27 +39,27 @@ export const WalletInfo: React.FC<WalletInfoProps> = ({ cart, currentUser, walle
* If the currently connected user (i.e. the operator), is an admin or a manager, he may book the reservation for someone else.
*/
const isOperatorAndClient = (): boolean => {
return currentUser.id == cart.customer_id;
}
return currentUser.id === cart.customer_id;
};
/**
* If the client has some money in his wallet & the price is not zero, then we should display this component.
*/
const shouldBeShown = (): boolean => {
return wallet.amount > 0 && price > 0;
}
};
/**
* If the amount in the wallet is not enough to cover the whole price, then the user must pay the remaining price
* using another payment mean.
*/
const hasRemainingPrice = (): boolean => {
return remainingPrice > 0;
}
};
/**
* Does the current cart contains a payment schedule?
*/
const isPaymentSchedule = (): boolean => {
return cart.items.find(i => 'subscription' in i) && cart.payment_schedule;
}
};
/**
* Return the human-readable name of the item currently bought with the wallet
*/
@ -74,15 +74,15 @@ export const WalletInfo: React.FC<WalletInfoProps> = ({ cart, currentUser, walle
}
return t(`app.shared.wallet.wallet_info.item_${item}`);
}
};
return (
<div className="wallet-info">
{shouldBeShown() && <div>
{isOperatorAndClient() && <div>
<h3>{t('app.shared.wallet.wallet_info.you_have_AMOUNT_in_wallet', {AMOUNT: FormatLib.price(wallet.amount)})}</h3>
<h3>{t('app.shared.wallet.wallet_info.you_have_AMOUNT_in_wallet', { AMOUNT: FormatLib.price(wallet.amount) })}</h3>
{!hasRemainingPrice() && <p>
{t('app.shared.wallet.wallet_info.wallet_pay_ITEM', {ITEM: getPriceItem()})}
{t('app.shared.wallet.wallet_info.wallet_pay_ITEM', { ITEM: getPriceItem() })}
</p>}
{hasRemainingPrice() && <p>
{t('app.shared.wallet.wallet_info.credit_AMOUNT_for_pay_ITEM', {
@ -92,9 +92,9 @@ export const WalletInfo: React.FC<WalletInfoProps> = ({ cart, currentUser, walle
</p>}
</div>}
{!isOperatorAndClient() && <div>
<h3>{t('app.shared.wallet.wallet_info.client_have_AMOUNT_in_wallet', {AMOUNT: FormatLib.price(wallet.amount)})}</h3>
<h3>{t('app.shared.wallet.wallet_info.client_have_AMOUNT_in_wallet', { AMOUNT: FormatLib.price(wallet.amount) })}</h3>
{!hasRemainingPrice() && <p>
{t('app.shared.wallet.wallet_info.client_wallet_pay_ITEM', {ITEM: getPriceItem()})}
{t('app.shared.wallet.wallet_info.client_wallet_pay_ITEM', { ITEM: getPriceItem() })}
</p>}
{hasRemainingPrice() && <p>
{t('app.shared.wallet.wallet_info.client_credit_AMOUNT_for_pay_ITEM', {
@ -110,7 +110,7 @@ export const WalletInfo: React.FC<WalletInfoProps> = ({ cart, currentUser, walle
</div>}
</div>
);
}
};
const WalletInfoWrapper: React.FC<WalletInfoProps> = ({ currentUser, cart, price, wallet }) => {
return (
@ -118,6 +118,6 @@ const WalletInfoWrapper: React.FC<WalletInfoProps> = ({ currentUser, cart, price
<WalletInfo currentUser={currentUser} cart={cart} price={price} wallet={wallet}/>
</Loader>
);
}
};
Application.Components.component('walletInfo', react2angular(WalletInfoWrapper, ['currentUser', 'price', 'cart', 'wallet']));

View File

@ -179,11 +179,9 @@ export interface KryptonConfig {
}
type DefaultCallback = () => void
// eslint-disable-next-line no-use-before-define
type BrandChangedCallback = (event: { KR: KryptonClient, cardInfo: { brand: string } }) => void
type ErrorCallback = (event: KryptonError) => void
type FocusCallback = (event: KryptonFocus) => void
// eslint-disable-next-line no-use-before-define
type InstallmentChangedCallback = (event: { KR: KryptonClient, installmentInfo: { brand: string, hasInterests: boolean, installmentCount: number, totalAmount: number } }) => void
type SubmitCallback = (event: ProcessPaymentAnswer) => boolean
type ClickCallback = (event: unknown) => boolean