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

Merge branch 'dev' for release 5.4.5

This commit is contained in:
Sylvain 2022-06-27 15:25:53 +02:00
commit 9f7504dfe9
201 changed files with 4234 additions and 2473 deletions

View File

@ -19,7 +19,7 @@
"$": true,
"KeyboardEvent": true
},
"plugins": ["html-erb"],
"plugins": ["html-erb", "fabmanager"],
"overrides": [
{
"files": ["**/*.ts", "**/*.tsx"],
@ -46,6 +46,20 @@
"react/prop-types": "off"
}
},
{
"files": ["app/frontend/src/javascript/components/**/*.tsx"],
"rules": {
"import/no-default-export": "error",
"import/no-unused-modules": "error",
"fabmanager/component-class-named-as-component": ["error", { "ignoreAbstractKeyword": true }],
"fabmanager/component-named-like-file": "error",
"fabmanager/component-documentation": "warn",
"fabmanager/component-methods-documentation": "warn",
"fabmanager/no-bootstrap": "error",
"fabmanager/no-utilities": "error",
"fabmanager/scoped-translation": ["error", { "ignoreAbstractKeyword": false }]
}
},
{
"files": ["app/frontend/src/javascript/models/**/*.ts"],
"rules": {

View File

@ -2,6 +2,39 @@
## next deploy
## v5.4.5 2022 June 27
- Feature the next event in the event page
- Documentation for installing behind a proxy
- Ability to install behind a proxy
- Improved docker image building time
- Use relative paths in mount scripts
- Run the docker image with the system user
- During the setup, autoconfigure the main domain
- During the setup, ask to set ALLOW_INSECURE_HTTP if DEFAULT_PROTOCOL was set to http
- Override angular currency filter, use Intl.NumberFormat to format amount
- Added some eslint rules to validate react components code style
- Fixed all react components code according to eslint rules
- Renamed proof-of-identity to supporting-documents in react components and in end-user strings
- Use bat to display coloured documentation of environment variables during setup
- Check the minimum docker version (20.10) during setup and upgrade
- Fix a bug: when email was mapped from SSO provided as empty string -> unable to merge account
- Fix a bug: when an empty data was retured by the SSO, unable to edit it
- Fix a bug: user can change his group in the profile completion page, even if mapped from the SSO
- Fix a bug: the birthdate was not marked as required, in the profile edition form
- Fix a bug: when the phone or the address were required, they were not marked as this, in the profile edition form
- Fix a bug: the birthday was not shown in user edition form
- Fix a bug: canceled event label's translation
- Fix a bug: unable to set the twitter input empty
- Fix a bug: unable to edit an event
- Fix a bug: times are not shown in admin/events monitoring page
- Fix a bug: unable to generate the secret key base during the setup
- Fix a bug: error message during the setup: the input device is not a TTY
- Fix a bug: when Fab-manager was installed as non-root user, unable to compile the assets during the upgrade
- Fix a bug: unable to remove an SSO data mapping field once saved
- Fix a bug: unable to update the user profile after toggling the organization switch twice
- [TODO DEPLOY] `\curl -sSL https://raw.githubusercontent.com/sleede/fab-manager/master/scripts/set-docker-user.sh | bash`
## v5.4.4 2022 June 8
- Check shopping cart items are valid before online payment

View File

@ -44,14 +44,19 @@ RUN bundle config --global frozen 1
# Install gems in a cache efficient way
WORKDIR /tmp
COPY Gemfile /tmp/
COPY Gemfile.lock /tmp/
COPY Gemfile* /tmp/
RUN bundle config set --local without 'development test doc' && bundle install && bundle binstubs --all
# Prepare the application directories
RUN mkdir -p /var/log/supervisor && \
mkdir -p /usr/src/app/tmp/sockets && \
mkdir -p /usr/src/app/tmp/pids
mkdir -p /usr/src/app/tmp/pids && \
mkdir -p /usr/src/app/tmp/cache && \
mkdir -p /usr/src/app/log && \
mkdir -p /usr/src/app/node_modules && \
mkdir -p /usr/src/app/public/api && \
chmod -R a+w /usr/src/app && \
chmod -R a+w /var/run
# Install Javascript packages
WORKDIR /usr/src/app
@ -65,23 +70,24 @@ RUN apk del .build-deps && \
rm -rf /tmp/* \
/var/tmp/* \
/var/cache/apk/* \
/usr/lib/ruby/gems/*/cache/*
/usr/lib/ruby/gems/*/cache/* && \
chmod -R a+w /usr/src/app/node_modules
# Copy source files
COPY docker/database.yml /usr/src/app/config/database.yml
COPY . /usr/src/app
# Volumes (the folders are created by setup.sh)
VOLUME /usr/src/app/invoices
VOLUME /usr/src/app/payment_schedules
VOLUME /usr/src/app/exports
VOLUME /usr/src/app/imports
VOLUME /usr/src/app/public
VOLUME /usr/src/app/public/uploads
VOLUME /usr/src/app/public/packs
VOLUME /usr/src/app/accounting
VOLUME /usr/src/app/proof_of_identity_files
VOLUME /var/log/supervisor
VOLUME /usr/src/app/invoices \
/usr/src/app/payment_schedules \
/usr/src/app/exports \
/usr/src/app/imports \
/usr/src/app/public \
/usr/src/app/public/uploads \
/usr/src/app/public/packs \
/usr/src/app/accounting \
/usr/src/app/proof_of_identity_files \
/var/log/supervisor
# Expose port 3000 to the Docker host, so we can access it from the outside
EXPOSE 3000

View File

@ -1,3 +1,3 @@
#web: bundle exec rails server puma -p $PORT
web: bundle exec rails server puma -p $PORT
worker: bundle exec sidekiq -C ./config/sidekiq.yml
webpack: bin/webpacker-dev-server

View File

@ -267,7 +267,7 @@ class API::MembersController < API::ApiController
:dailymotion, :github, :echosciences, :pinterest, :lastfm, :flickr,
user_avatar_attributes: %i[id attachment destroy]],
invoicing_profile_attributes: [
:id,
:id, :organization,
address_attributes: %i[id address],
organization_attributes: [:id, :name, address_attributes: %i[id address]],
user_profile_custom_fields_attributes: %i[id value invoicing_profile_id profile_custom_field_id]
@ -282,7 +282,7 @@ class API::MembersController < API::ApiController
:dailymotion, :github, :echosciences, :pinterest, :lastfm, :flickr,
user_avatar_attributes: %i[id attachment destroy]],
invoicing_profile_attributes: [
:id,
:id, :organization,
address_attributes: %i[id address],
organization_attributes: [:id, :name, address_attributes: %i[id address]],
user_profile_custom_fields_attributes: %i[id value invoicing_profile_id profile_custom_field_id]

View File

@ -14,4 +14,5 @@ These components must be written using the following conventions:
- Depending on if we want to use the `<Suspense>` wrapper or not, we can export the component directly or wrap it in a `<Loader>` wrapper.
- When a component is used in angularJS, the wrapper is required. The component must be named like `const Foo` (no export if not used in React) and must have a `const FooWrapper` at the end of its file, which wraps the component in a `<Loader>`.
- Translations must be grouped per component. For example, the `FooBar` component must have its translations in the `config/locales/app.$SCOPE.en.yml` file, under the `foo_bar` key.
- Most of these rules are validated by eslint-plugin-fabmanager. Please ensure you write eslint valid code, and think twice you have a very good reason before disabling any rule.

View File

@ -1,5 +1,5 @@
/**
* This is a compatibility wrapper to allow usage of react-switch inside of the angular.js app
* This is a compatibility wrapper to allow usage of react-switch inside the angular.js app
*/
import Switch from 'react-switch';
import { react2angular } from 'react2angular';

View File

@ -1,9 +1,9 @@
import React, { useEffect, useState } from 'react';
import { UseFormRegister, useFieldArray, ArrayPath, useWatch, Path } from 'react-hook-form';
import { UseFormRegister, useFieldArray, ArrayPath, useWatch, Path, FieldPathValue } from 'react-hook-form';
import { FieldValues } from 'react-hook-form/dist/types/fields';
import AuthProviderAPI from '../../api/auth-provider';
import { AuthenticationProviderMapping, MappingFields, mappingType, ProvidableType } from '../../models/authentication-provider';
import { Control, UseFormSetValue } from 'react-hook-form/dist/types/form';
import { Control, UnpackNestedValue, UseFormSetValue } from 'react-hook-form/dist/types/form';
import { FormSelect } from '../form/form-select';
import { FormInput } from '../form/form-input';
import { useTranslation } from 'react-i18next';
@ -96,6 +96,31 @@ export const DataMappingForm = <TFieldValues extends FieldValues, TContext exten
};
};
/**
* Remove the data whom index is provided: mark it as "to destroy" or simply remove it if it was unsaved
*/
const removeMapping = (index: number): void => {
if (currentFormValues[index].id) {
setValue(
`auth_provider_mappings_attributes.${index}._destroy` as Path<TFieldValues>,
true as UnpackNestedValue<FieldPathValue<TFieldValues, Path<TFieldValues>>>
);
} else {
remove(index);
}
};
/**
* Return a className based on the current mapping-item status
*/
const itemStatus = (index: number): string => {
if (currentFormValues[index]?.id) {
if (currentFormValues[index]._destroy) return 'destroyed-item';
return 'saved-item';
}
return 'new-item';
};
// fetch the mapping data from the API on mount
useEffect(() => {
AuthProviderAPI.mappingFields().then((data) => {
@ -114,7 +139,7 @@ export const DataMappingForm = <TFieldValues extends FieldValues, TContext exten
</FabButton>
</div>
{fields.map((item, index) => (
<div key={item.id} className="mapping-item">
<div key={item.id} className={`mapping-item ${itemStatus(index)}`}>
<div className="inputs">
<FormInput id={`auth_provider_mappings_attributes.${index}.id`} register={register} type="hidden" />
<div className="local-data">
@ -141,7 +166,7 @@ export const DataMappingForm = <TFieldValues extends FieldValues, TContext exten
onClick={toggleTypeMappingModal(index)}
disabled={getField(output, index) === undefined}
tooltip={t('app.admin.authentication.data_mapping_form.data_mapping')} />
<FabButton icon={<i className="fa fa-trash" />} onClick={() => remove(index)} className="delete-button" />
<FabButton icon={<i className="fa fa-trash" />} onClick={() => removeMapping(index)} className="delete-button" />
<TypeMappingModal model={getModel(output, index)}
field={getField(output, index)}
type={getDataType(output, index)}

View File

@ -13,6 +13,10 @@ interface Oauth2DataMappingFormProps<TFieldValues, TContext extends object> {
index: number,
}
/**
* Partial form to set the data mapping for an OAuth 2.0 provider.
* The data mapping is the way to bind data from the authentication provider API to the Fab-manager's database
*/
export const Oauth2DataMappingForm = <TFieldValues extends FieldValues, TContext extends object>({ register, control, index }: Oauth2DataMappingFormProps<TFieldValues, TContext>) => {
const { t } = useTranslation('admin');

View File

@ -16,6 +16,10 @@ interface OpenidConnectDataMappingFormProps<TFieldValues> {
index: number,
}
/**
* Partial form to set the data mapping for an OpenID Connect provider.
* The data mapping is the way to bind data from the OIDC claims to the Fab-manager's database
*/
export const OpenidConnectDataMappingForm = <TFieldValues extends FieldValues>({ register, setValue, currentFormValues, index }: OpenidConnectDataMappingFormProps<TFieldValues>) => {
const { t } = useTranslation('admin');
@ -40,6 +44,7 @@ export const OpenidConnectDataMappingForm = <TFieldValues extends FieldValues>({
const model = currentFormValues[index]?.local_model;
const field = currentFormValues[index]?.local_field;
const configuration = standardConfiguration[`${model}.${field}`];
if (configuration) {
setValue(
`auth_provider_mappings_attributes.${index}.api_field` as Path<TFieldValues>,
configuration.api_field as UnpackNestedValue<FieldPathValue<TFieldValues, Path<TFieldValues>>>
@ -52,6 +57,7 @@ export const OpenidConnectDataMappingForm = <TFieldValues extends FieldValues>({
);
});
}
}
};
return (

View File

@ -20,6 +20,9 @@ interface OpenidConnectFormProps<TFieldValues, TContext extends object> {
setValue: UseFormSetValue<TFieldValues>,
}
/**
* Partial form to fill the OpenID Connect (OIDC) settings for a new/existing authentication provider.
*/
export const OpenidConnectForm = <TFieldValues extends FieldValues, TContext extends object>({ register, control, currentFormValues, formState, setValue }: OpenidConnectFormProps<TFieldValues, TContext>) => {
const { t } = useTranslation('admin');

View File

@ -6,7 +6,7 @@ type inputType = string|number|readonly string [];
interface FabInputProps {
id: string,
onChange?: (value: inputType, validity?: ValidityState) => void,
defaultValue: inputType,
defaultValue?: inputType,
icon?: ReactNode,
addOn?: ReactNode,
addOnClassName?: string,

View File

@ -45,7 +45,7 @@ export const FabModal: React.FC<FabModalProps> = ({ title, isOpen, toggleModal,
className={`fab-modal fab-modal-${width} ${className}`}
overlayClassName="fab-modal-overlay"
onRequestClose={toggleModal}>
{closeButton && <FabButton className="modal-btn--close" onClick={toggleModal}>{t('app.shared.buttons.close')}</FabButton>}
{closeButton && <FabButton className="modal-btn--close" onClick={toggleModal}>{t('app.shared.fab_modal.close')}</FabButton>}
<div className="fab-modal-header">
{!customHeader && <h1>{ title }</h1>}
{customHeader && customHeader}

View File

@ -0,0 +1,28 @@
import React, { ReactNode } from 'react';
interface FabPanelProps {
className?: string,
header?: ReactNode,
size?: 'small' | 'normal'
}
/**
* Simple styled panel component
*/
export const FabPanel: React.FC<FabPanelProps> = ({ className, header, size, children }) => {
return (
<div className={`fab-panel ${className || ''}`}>
{header && <div>
<div className={`panel-header ${size}`}>
{header}
</div>
<div className="panel-content">
{children}
</div>
</div>}
{!header && <div className="no-header">
{children}
</div>}
</div>
);
};

View File

@ -15,9 +15,9 @@ interface LabelledInputProps {
*/
export const LabelledInput: React.FC<LabelledInputProps> = ({ id, type, label, value, onChange }) => {
return (
<div className="input-with-label">
<label className="label" htmlFor={id}>{label}</label>
<input className="input" id={id} type={type} value={value} onChange={onChange} />
<div className="labelled-input">
<label htmlFor={id}>{label}</label>
<input id={id} type={type} value={value} onChange={onChange} />
</div>
);
};

View File

@ -32,7 +32,7 @@ export interface FabTextEditorRef {
*/
export const FabTextEditor: React.ForwardRefRenderFunction<FabTextEditorRef, FabTextEditorProps> = ({ paragraphTools, content, limit = 400, video, image, onChange, placeholder, error, disabled = false }, ref: RefObject<FabTextEditorRef>) => {
const { t } = useTranslation('shared');
const placeholderText = placeholder || t('app.shared.text_editor.text_placeholder');
const placeholderText = placeholder || t('app.shared.text_editor.fab_text_editor.text_placeholder');
// TODO: Add ctrl+click on link to visit
const editorRef: React.MutableRefObject<Editor | null> = useRef(null);
@ -66,7 +66,7 @@ export const FabTextEditor: React.ForwardRefRenderFunction<FabTextEditorRef, Fab
Iframe,
Image.configure({
HTMLAttributes: {
class: 'fab-textEditor-image'
class: 'fab-text-editor-image'
}
})
],
@ -85,14 +85,14 @@ export const FabTextEditor: React.ForwardRefRenderFunction<FabTextEditorRef, Fab
editorRef.current = editor;
return (
<div className={`fab-textEditor ${disabled && 'is-disabled'}`}>
<div className={`fab-text-editor ${disabled && 'is-disabled'}`}>
<MenuBar editor={editor} paragraphTools={paragraphTools} video={video} image={image} disabled={disabled} />
<EditorContent editor={editor} />
<div className="fab-textEditor-character-count">
<div className="fab-text-editor-character-count">
{editor?.storage.characterCount.characters()} / {limit}
</div>
{error &&
<div className="fab-textEditor-error">
<div className="fab-text-editor-error">
<WarningOctagon size={24} />
<p className="">{error}</p>
</div>
@ -101,4 +101,5 @@ export const FabTextEditor: React.ForwardRefRenderFunction<FabTextEditorRef, Fab
);
};
// eslint-disable-next-line import/no-default-export
export default forwardRef(FabTextEditor);

View File

@ -18,6 +18,7 @@ declare module '@tiptap/core' {
}
}
// eslint-disable-next-line import/no-default-export
export default Node.create<IframeOptions>({
name: 'iframe',
@ -29,7 +30,7 @@ export default Node.create<IframeOptions>({
return {
allowFullscreen: true,
HTMLAttributes: {
class: 'fab-textEditor-video'
class: 'fab-text-editor-video'
}
};
},

View File

@ -141,7 +141,7 @@ export const MenuBar: React.FC<MenuBarProps> = ({ editor, paragraphTools, video,
return (
<>
<div className={`fab-textEditor-menu ${disabled ? 'fab-textEditor-menu--disabled' : ''}`}>
<div className={`fab-text-editor-menu ${disabled ? 'fab-text-editor-menu--disabled' : ''}`}>
{ paragraphTools &&
(<>
<button
@ -168,7 +168,7 @@ export const MenuBar: React.FC<MenuBarProps> = ({ editor, paragraphTools, video,
>
<Quotes size={24} />
</button>
<span className='divider'></span>
<span className='menu-divider'></span>
</>)
}
<button
@ -203,7 +203,7 @@ export const MenuBar: React.FC<MenuBarProps> = ({ editor, paragraphTools, video,
>
<LinkSimpleHorizontal size={24} />
</button>
{ (video || image) && <span className='divider'></span> }
{ (video || image) && <span className='menu-divider'></span> }
{ video &&
(<>
<button
@ -228,19 +228,19 @@ export const MenuBar: React.FC<MenuBarProps> = ({ editor, paragraphTools, video,
}
</div>
<div ref={ref} className={`fab-textEditor-subMenu ${submenu ? 'is-active' : ''}`}>
<div ref={ref} className={`fab-text-editor-subMenu ${submenu ? 'is-active' : ''}`}>
{ submenu === 'link' &&
(<>
<h6>{t('app.shared.text_editor.add_link')}</h6>
<h6>{t('app.shared.text_editor.menu_bar.add_link')}</h6>
<div>
<input value={url.href} onChange={linkUrlChange} onKeyDown={handleEnter} type="text" placeholder={t('app.shared.text_editor.link_placeholder')} />
<input value={url.href} onChange={linkUrlChange} onKeyDown={handleEnter} type="text" placeholder={t('app.shared.text_editor.menu_bar.link_placeholder')} />
<button type='button' onClick={unsetLink}>
<Trash size={24} />
</button>
</div>
<div>
<label className='tab'>
<p>{t('app.shared.text_editor.new_tab')}</p>
<p>{t('app.shared.text_editor.menu_bar.new_tab')}</p>
<input type="checkbox" onChange={toggleTarget} checked={url.target === '_blank'} />
<span className='switch'></span>
</label>
@ -252,14 +252,14 @@ export const MenuBar: React.FC<MenuBarProps> = ({ editor, paragraphTools, video,
}
{ submenu === 'video' &&
(<>
<h6>{t('app.shared.text_editor.add_video')}</h6>
<h6>{t('app.shared.text_editor.menu_bar.add_video')}</h6>
<select name="provider" onChange={handleSelect}>
<option value="youtube">YouTube</option>
<option value="vimeo">Vimeo</option>
<option value="dailymotion">Dailymotion</option>
</select>
<div>
<input type="text" onChange={videoUrlChange} placeholder={t('app.shared.text_editor.url_placeholder')} />
<input type="text" onChange={videoUrlChange} placeholder={t('app.shared.text_editor.menu_bar.url_placeholder')} />
<button type='button' onClick={() => addIframe()}>
<CheckCircle size={24} />
</button>
@ -268,9 +268,9 @@ export const MenuBar: React.FC<MenuBarProps> = ({ editor, paragraphTools, video,
}
{ submenu === 'image' &&
(<>
<h6>{t('app.shared.text_editor.add_image')}</h6>
<h6>{t('app.shared.text_editor.menu_bar.add_image')}</h6>
<div>
<input type="text" onChange={imageUrlChange} placeholder={t('app.shared.text_editor.url_placeholder')} />
<input type="text" onChange={imageUrlChange} placeholder={t('app.shared.text_editor.menu_bar.url_placeholder')} />
<button type='button' onClick={() => addImage()}>
<CheckCircle size={24} />
</button>

View File

@ -0,0 +1,138 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { react2angular } from 'react2angular';
import { IApplication } from '../../models/application';
import { Loader } from '../base/loader';
import { Event } from '../../models/event';
import FormatLib from '../../lib/format';
declare const Application: IApplication;
interface EventCardProps {
event: Event,
cardType: 'sm' | 'md' | 'lg'
}
/**
* This component is a box showing the picture of the given event, and a short description of it.
*/
export const EventCard: React.FC<EventCardProps> = ({ event, cardType }) => {
const { t } = useTranslation('public');
/**
* Format description to remove HTML tags and set a maximum character count
*/
const formatText = (text: string, count: number) => {
text = text.replace(/(<\/p>|<\/h4>|<\/h5>|<\/h6>|<\/pre>|<\/blockquote>)/g, '\n');
text = text.replace(/<br\s*\/?>/g, '\n');
text = text.replace(/<\/?\w+[^>]*>/g, '');
if (text.length > count) {
text = text.slice(0, count) + '…';
}
text = text.replace(/\n+/g, '<br />');
return text;
};
/**
* Return the formatted localized date of the event
*/
const formatDate = (): string => {
const startDate = new Date(event.start_date);
const endDate = new Date(event.end_date);
const singleDayEvent = startDate.getFullYear() === endDate.getFullYear() &&
startDate.getMonth() === endDate.getMonth() &&
startDate.getDate() === endDate.getDate();
return singleDayEvent
? t('app.public.event_card.on_the_date', { DATE: FormatLib.date(event.start_date) })
: t('app.public.event_card.from_date_to_date', { START: FormatLib.date(event.start_date), END: FormatLib.date(event.end_date) });
};
/**
* Return the formatted localized hours of the event
*/
const formatTime = (): string => {
return event.all_day
? t('app.public.event_card.all_day')
: t('app.public.event_card.from_time_to_time', { START: FormatLib.time(event.start_date), END: FormatLib.time(event.end_date) });
};
return (
<div className={`event-card event-card--${cardType}`}>
{event.event_image
? <div className="event-card-picture">
<img src={event.event_image} alt="" />
</div>
: cardType !== 'sm' &&
<div className="event-card-picture">
<i className="fas fa-image"></i>
</div>
}
<div className="event-card-desc">
<header>
<span className={`badge bg-${event.category.slug}`}>{event.category.name}</span>
<p className='title'>{event?.title}</p>
</header>
{cardType !== 'sm' &&
<p dangerouslySetInnerHTML={{ __html: formatText(event.description, cardType === 'md' ? 500 : 400) }}></p>
}
</div>
<div className="event-card-info">
{cardType !== 'md' &&
<p>
{formatDate()}
<span>{formatTime()}</span>
</p>
}
<div className="grid">
{cardType !== 'md' &&
event.event_themes.map(theme => {
return (<div key={theme.name} className="grid-item">
<i className="fa fa-tags"></i>
<h6>{theme.name}</h6>
</div>);
})
}
{(cardType !== 'md' && event.age_range) &&
<div className="grid-item">
<i className="fa fa-users"></i>
<h6>{event.age_range?.name}</h6>
</div>
}
{cardType === 'md' &&
<>
<div className="grid-item">
<i className="fa fa-calendar"></i>
<h6>{formatDate()}</h6>
</div>
<div className="grid-item">
<i className="fa fa-clock"></i>
<h6>{formatTime()}</h6>
</div>
</>
}
<div className="grid-item">
<i className="fa fa-user"></i>
{event.nb_free_places > 0 && <h6>{t('app.public.event_card.still_available') + event.nb_free_places}</h6>}
{event.nb_total_places > 0 && event.nb_free_places <= 0 && <h6>{t('app.public.event_card.event_full')}</h6>}
{!event.nb_total_places && <h6>{t('app.public.event_card.without_reservation')}</h6>}
</div>
<div className="grid-item">
<i className="fa fa-bookmark"></i>
{event.amount === 0 && <h6>{t('app.public.event_card.free_admission')}</h6>}
{event.amount > 0 && <h6>{t('app.public.event_card.full_price') + FormatLib.price(event.amount)}</h6>}
</div>
</div>
</div>
</div>
);
};
const EventCardWrapper: React.FC<EventCardProps> = ({ event, cardType }) => {
return (
<Loader>
<EventCard event={event} cardType={cardType} />
</Loader>
);
};
Application.Components.component('eventCard', react2angular(EventCardWrapper, ['event', 'cardType']));

View File

@ -2,11 +2,11 @@ import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import Select from 'react-select';
import { react2angular } from 'react2angular';
import { Loader } from './base/loader';
import { Event } from '../models/event';
import { EventTheme } from '../models/event-theme';
import { IApplication } from '../models/application';
import EventThemeAPI from '../api/event-theme';
import { Loader } from '../base/loader';
import { Event } from '../../models/event';
import { EventTheme } from '../../models/event-theme';
import { IApplication } from '../../models/application';
import EventThemeAPI from '../../api/event-theme';
declare const Application: IApplication;
@ -24,7 +24,7 @@ type selectOption = { value: number, label: string };
/**
* This component shows a select input to edit the themes associated with the event
*/
const EventThemes: React.FC<EventThemesProps> = ({ event, onChange }) => {
export const EventThemes: React.FC<EventThemesProps> = ({ event, onChange }) => {
const { t } = useTranslation('shared');
const [themes, setThemes] = useState<Array<EventTheme>>([]);
@ -77,10 +77,10 @@ const EventThemes: React.FC<EventThemesProps> = ({ event, onChange }) => {
return (
<div className="event-themes">
{hasThemes() && <div className="event-themes--panel">
<h3>{ t('app.shared.event.event_themes') }</h3>
<h3>{ t('app.shared.event_themes.title') }</h3>
<div className="content">
<Select defaultValue={defaultValues()}
placeholder={t('app.shared.event.select_theme')}
placeholder={t('app.shared.event_themes.select_theme')}
onChange={handleChange}
options={buildOptions()}
isMulti />

View File

@ -2,7 +2,7 @@
This directory is holding the inputs components for usage within forms controlled by [React-hook-form](https://react-hook-form.com/).
All these components must have [props](https://reactjs.org/docs/components-and-props.html) that inherits from [FormComponent](../models/form-component.ts)
All these components must have [props](https://reactjs.org/docs/components-and-props.html) that inherit from [FormComponent](../models/form-component.ts)
or from [FormControlledComponent](../models/form-component.ts).
Please look at the existing components for examples.

View File

@ -40,7 +40,6 @@ export const AbstractFormItem = <TFieldValues extends FieldValues>({ id, label,
// Compose classnames from props
const classNames = [
'form-item',
`${className || ''}`,
`${isDirty && fieldError ? 'is-incorrect' : ''}`,
`${isDirty && warning ? 'is-warned' : ''}`,
@ -59,7 +58,7 @@ export const AbstractFormItem = <TFieldValues extends FieldValues>({ id, label,
}
return (
<label className={classNames} onClick={handleLabelClick}>
<label className={`form-item ${classNames}`} onClick={handleLabelClick}>
{label && <div className='form-item-header'>
<p>{label}</p>
{tooltip && <div className="item-tooltip">

View File

@ -44,14 +44,13 @@ export const FormInput = <TFieldValues extends FieldValues, TInputType>({ id, re
// Compose classnames from props
const classNames = [
'form-input',
`${className || ''}`,
`${type === 'hidden' ? 'is-hidden' : ''}`
].join(' ');
return (
<AbstractFormItem id={id} formState={formState} label={label}
className={classNames} tooltip={tooltip}
className={`form-input ${classNames}`} tooltip={tooltip}
disabled={disabled}
rules={rules} error={error} warning={warning}>
{icon && <span className="icon">{icon}</span>}

View File

@ -30,6 +30,9 @@ interface ChangeGroupProps {
*/
type selectOption = { value: number, label: string };
/**
* Component to display the group of the provided user, and allow him to change his group.
*/
export const ChangeGroup: React.FC<ChangeGroupProps> = ({ user, onSuccess, onError, allowChange, className }) => {
const { t } = useTranslation('shared');

View File

@ -21,7 +21,7 @@ interface MachineCardProps {
* This component is a box showing the picture of the given machine and two buttons: one to start the reservation process
* and another to redirect the user to the machine description page.
*/
const MachineCardComponent: React.FC<MachineCardProps> = ({ user, machine, onShowMachine, onReserveMachine, onError, onSuccess, onLoginRequested, onEnrollRequested, canProposePacks }) => {
const MachineCard: React.FC<MachineCardProps> = ({ user, machine, onShowMachine, onReserveMachine, onError, onSuccess, onLoginRequested, onEnrollRequested, canProposePacks }) => {
const { t } = useTranslation('public');
// shall we display a loader to prevent double-clicking, while the machine details are loading?
@ -40,6 +40,9 @@ const MachineCardComponent: React.FC<MachineCardProps> = ({ user, machine, onSho
onShowMachine(machine);
};
/**
* Return the machine's picture or a placeholder
*/
const machinePicture = (): ReactNode => {
if (!machine.machine_image) {
return <div className="machine-picture no-picture" />;
@ -82,10 +85,12 @@ const MachineCardComponent: React.FC<MachineCardProps> = ({ user, machine, onSho
);
};
export const MachineCard: React.FC<MachineCardProps> = ({ user, machine, onShowMachine, onReserveMachine, onError, onSuccess, onLoginRequested, onEnrollRequested, canProposePacks }) => {
const MachineCardWrapper: React.FC<MachineCardProps> = (props) => {
return (
<Loader>
<MachineCardComponent user={user} machine={machine} onShowMachine={onShowMachine} onReserveMachine={onReserveMachine} onError={onError} onSuccess={onSuccess} onLoginRequested={onLoginRequested} onEnrollRequested={onEnrollRequested} canProposePacks={canProposePacks} />
<MachineCard {...props} />
</Loader>
);
};
export { MachineCardWrapper as MachineCard };

View File

@ -12,6 +12,9 @@ interface MachinesFiltersProps {
*/
type selectOption = { value: boolean, label: string };
/**
* Allows filtering on machines list
*/
export const MachinesFilters: React.FC<MachinesFiltersProps> = ({ onStatusSelected }) => {
const { t } = useTranslation('public');

View File

@ -24,7 +24,7 @@ interface MachinesListProps {
/**
* This component shows a list of all machines and allows filtering on that list.
*/
const MachinesList: React.FC<MachinesListProps> = ({ onError, onSuccess, onShowMachine, onReserveMachine, onLoginRequested, onEnrollRequested, user, canProposePacks }) => {
export const MachinesList: React.FC<MachinesListProps> = ({ onError, onSuccess, onShowMachine, onReserveMachine, onLoginRequested, onEnrollRequested, user, canProposePacks }) => {
// shown machines
const [machines, setMachines] = useState<Array<Machine>>(null);
// we keep the full list of machines, for filtering

View File

@ -3,11 +3,12 @@ import { FabModal } from '../base/fab-modal';
import { useTranslation } from 'react-i18next';
import { HtmlTranslate } from '../base/html-translate';
import FormatLib from '../../lib/format';
import { TDateISO } from '../../typings/date-iso';
interface PendingTrainingModalProps {
isOpen: boolean,
toggleModal: () => void,
nextReservation: Date,
nextReservation: TDateISO,
}
/**
@ -20,7 +21,7 @@ export const PendingTrainingModal: React.FC<PendingTrainingModalProps> = ({ isOp
/**
* Return the formatted localized date for the given date
*/
const formatDateTime = (date: Date): string => {
const formatDateTime = (date: TDateISO): string => {
return t('app.logged.pending_training_modal.DATE_TIME', { DATE: FormatLib.date(date), TIME: FormatLib.time(date) });
};

View File

@ -42,7 +42,7 @@ export const RequiredTrainingModal: React.FC<RequiredTrainingModalProps> = ({ is
const header = (): ReactNode => {
return (
<div className="user-info">
<Avatar userName={user?.name} avatar={user?.profile_attributes?.user_avatar_attributes?.attachment} />
<Avatar userName={user?.name} avatar={user?.profile_attributes?.user_avatar_attributes?.attachment_url} />
<span className="user-name">{user?.name}</span>
</div>
);

View File

@ -31,7 +31,7 @@ interface ReserveButtonProps {
/**
* Button component that makes the training verification before redirecting the user to the reservation calendar
*/
const ReserveButtonComponent: React.FC<ReserveButtonProps> = ({ currentUser, machineId, onLoginRequested, onLoadingStart, onLoadingEnd, onError, onSuccess, onReserveMachine, onEnrollRequested, className, children, canProposePacks }) => {
const ReserveButton: React.FC<ReserveButtonProps> = ({ currentUser, machineId, onLoginRequested, onLoadingStart, onLoadingEnd, onError, onSuccess, onReserveMachine, onEnrollRequested, className, children, canProposePacks }) => {
const { t } = useTranslation('shared');
const [machine, setMachine] = useState<Machine>(null);
@ -183,14 +183,16 @@ const ReserveButtonComponent: React.FC<ReserveButtonProps> = ({ currentUser, mac
);
};
export const ReserveButton: React.FC<ReserveButtonProps> = ({ currentUser, machineId, onLoginRequested, onLoadingStart, onLoadingEnd, onError, onSuccess, onReserveMachine, onEnrollRequested, className, children, canProposePacks }) => {
const ReserveButtonWrapper: React.FC<ReserveButtonProps> = (props) => {
return (
<Loader>
<ReserveButtonComponent currentUser={currentUser} machineId={machineId} onError={onError} onSuccess={onSuccess} onLoadingStart={onLoadingStart} onLoadingEnd={onLoadingEnd} onReserveMachine={onReserveMachine} onLoginRequested={onLoginRequested} onEnrollRequested={onEnrollRequested} className={className} canProposePacks={canProposePacks}>
{children}
</ReserveButtonComponent>
<ReserveButton {...props}>
{props.children}
</ReserveButton>
</Loader>
);
};
Application.Components.component('reserveButton', react2angular(ReserveButton, ['currentUser', 'machineId', 'onLoadingStart', 'onLoadingEnd', 'onError', 'onSuccess', 'onReserveMachine', 'onLoginRequested', 'onEnrollRequested', 'className', 'canProposePacks']));
export { ReserveButtonWrapper as ReserveButton };
Application.Components.component('reserveButton', react2angular(ReserveButtonWrapper, ['currentUser', 'machineId', 'onLoadingStart', 'onLoadingEnd', 'onError', 'onSuccess', 'onReserveMachine', 'onLoginRequested', 'onEnrollRequested', 'className', 'canProposePacks']));

View File

@ -40,29 +40,29 @@ export const PaymentScheduleSummary: React.FC<PaymentScheduleSummaryProps> = ({
return (
<div className="payment-schedule-summary">
<div>
<h4>{ t('app.shared.cart.your_payment_schedule') }</h4>
<h4>{ t('app.shared.payment_schedule_summary.your_payment_schedule') }</h4>
{hasEqualDeadlines() && <ul>
<li>
<span className="schedule-item-info">
{t('app.shared.cart.NUMBER_monthly_payment_of_AMOUNT', { NUMBER: schedule.items.length, AMOUNT: FormatLib.price(schedule.items[0].amount) })}
{t('app.shared.payment_schedule_summary.NUMBER_monthly_payment_of_AMOUNT', { NUMBER: schedule.items.length, AMOUNT: FormatLib.price(schedule.items[0].amount) })}
</span>
<span className="schedule-item-date">{t('app.shared.cart.first_debit')}</span>
<span className="schedule-item-date">{t('app.shared.payment_schedule_summary.first_debit')}</span>
</li>
</ul>}
{!hasEqualDeadlines() && <ul>
<li>
<span className="schedule-item-info">{t('app.shared.cart.monthly_payment_NUMBER', { NUMBER: 1 })}</span>
<span className="schedule-item-info">{t('app.shared.payment_schedule_summary.monthly_payment_NUMBER', { NUMBER: 1 })}</span>
<span className="schedule-item-price">{FormatLib.price(schedule.items[0].amount)}</span>
<span className="schedule-item-date">{t('app.shared.cart.debit')}</span>
<span className="schedule-item-date">{t('app.shared.payment_schedule_summary.debit')}</span>
</li>
<li>
<span className="schedule-item-info">
{t('app.shared.cart.NUMBER_monthly_payment_of_AMOUNT', { NUMBER: schedule.items.length - 1, AMOUNT: FormatLib.price(schedule.items[1].amount) })}
{t('app.shared.payment_schedule_summary.NUMBER_monthly_payment_of_AMOUNT', { NUMBER: schedule.items.length - 1, AMOUNT: FormatLib.price(schedule.items[1].amount) })}
</span>
</li>
</ul>}
<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}>
<button className="view-full-schedule" onClick={toggleFullScheduleModal}>{t('app.shared.payment_schedule_summary.view_full_schedule')}</button>
<FabModal title={t('app.shared.payment_schedule_summary.your_payment_schedule')} isOpen={modal} toggleModal={toggleFullScheduleModal}>
<ul className="full-schedule">
{schedule.items.map(item => (
<li key={String(item.due_date)}>

View File

@ -24,7 +24,7 @@ const PAGE_SIZE = 20;
* This component shows a list of all payment schedules with their associated deadlines (aka. PaymentScheduleItem) and invoices
* for the currentUser
*/
const PaymentSchedulesDashboard: React.FC<PaymentSchedulesDashboardProps> = ({ currentUser, onError, onCardUpdateSuccess }) => {
export const PaymentSchedulesDashboard: React.FC<PaymentSchedulesDashboardProps> = ({ currentUser, onError, onCardUpdateSuccess }) => {
const { t } = useTranslation('logged');
// list of displayed payment schedules
@ -66,7 +66,7 @@ const PaymentSchedulesDashboard: React.FC<PaymentSchedulesDashboardProps> = ({ c
* 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'));
onCardUpdateSuccess(t('app.logged.dashboard.payment_schedules_dashboard.card_updated_success'));
};
/**
@ -85,7 +85,7 @@ const PaymentSchedulesDashboard: React.FC<PaymentSchedulesDashboardProps> = ({ c
return (
<div className="payment-schedules-dashboard">
{!hasSchedules() && <div>{t('app.logged.dashboard.payment_schedules.no_payment_schedules')}</div>}
{!hasSchedules() && <div>{t('app.logged.dashboard.payment_schedules_dashboard.no_payment_schedules')}</div>}
{hasSchedules() && <div className="schedules-list">
<PaymentSchedulesTable paymentSchedules={paymentSchedules}
showCustomer={false}
@ -93,7 +93,7 @@ const PaymentSchedulesDashboard: React.FC<PaymentSchedulesDashboardProps> = ({ c
operator={currentUser}
onError={onError}
onCardUpdateSuccess={handleCardUpdateSuccess} />
{hasMoreSchedules() && <FabButton className="load-more" onClick={handleLoadMore}>{t('app.logged.dashboard.payment_schedules.load_more')}</FabButton>}
{hasMoreSchedules() && <FabButton className="load-more" onClick={handleLoadMore}>{t('app.logged.dashboard.payment_schedules_dashboard.load_more')}</FabButton>}
</div>}
</div>
);

View File

@ -25,7 +25,7 @@ const PAGE_SIZE = 20;
/**
* This component shows a list of all payment schedules with their associated deadlines (aka. PaymentScheduleItem) and invoices
*/
const PaymentSchedulesList: React.FC<PaymentSchedulesListProps> = ({ currentUser, onError, onCardUpdateSuccess }) => {
export const PaymentSchedulesList: React.FC<PaymentSchedulesListProps> = ({ currentUser, onError, onCardUpdateSuccess }) => {
const { t } = useTranslation('admin');
// list of displayed payment schedules
@ -93,19 +93,19 @@ const PaymentSchedulesList: React.FC<PaymentSchedulesListProps> = ({ currentUser
* 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'));
onCardUpdateSuccess(t('app.admin.invoices.payment_schedules_list.card_updated_success'));
};
return (
<div className="payment-schedules-list">
<h3>
<i className="fas fa-filter" />
{t('app.admin.invoices.payment_schedules.filter_schedules')}
{t('app.admin.invoices.payment_schedules_list.filter_schedules')}
</h3>
<div className="schedules-filters">
<DocumentFilters onFilterChange={handleFiltersChange} />
</div>
{!hasSchedules() && <div>{t('app.admin.invoices.payment_schedules.no_payment_schedules')}</div>}
{!hasSchedules() && <div>{t('app.admin.invoices.payment_schedules_list.no_payment_schedules')}</div>}
{hasSchedules() && <div className="schedules-list">
<PaymentSchedulesTable paymentSchedules={paymentSchedules}
showCustomer={true}
@ -113,7 +113,7 @@ const PaymentSchedulesList: React.FC<PaymentSchedulesListProps> = ({ currentUser
operator={currentUser}
onError={onError}
onCardUpdateSuccess={handleCardUpdateSuccess} />
{hasMoreSchedules() && <FabButton className="load-more" onClick={handleLoadMore}>{t('app.admin.invoices.payment_schedules.load_more')}</FabButton>}
{hasMoreSchedules() && <FabButton className="load-more" onClick={handleLoadMore}>{t('app.admin.invoices.payment_schedules_list.load_more')}</FabButton>}
</div>}
</div>
);

View File

@ -26,7 +26,7 @@ interface PaymentSchedulesTableProps {
/**
* This component shows a list of all payment schedules with their associated deadlines (aka. PaymentScheduleItem) and invoices
*/
const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({ paymentSchedules, showCustomer, refreshList, operator, onError, onCardUpdateSuccess }) => {
const PaymentSchedulesTable: React.FC<PaymentSchedulesTableProps> = ({ paymentSchedules, showCustomer, refreshList, operator, onError, onCardUpdateSuccess }) => {
const { t } = useTranslation('shared');
// for each payment schedule: are the details (all deadlines) shown or hidden?
@ -68,8 +68,10 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
*/
const expandCollapseIcon = (paymentScheduleId: number): JSX.Element => {
if (isExpanded(paymentScheduleId)) {
// eslint-disable-next-line fabmanager/component-class-named-as-component
return <i className="fas fa-minus-square" />;
} else {
// eslint-disable-next-line fabmanager/component-class-named-as-component
return <i className="fas fa-plus-square" />;
}
};
@ -93,9 +95,10 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
const downloadScheduleButton = (id: number): JSX.Element => {
const link = `api/payment_schedules/${id}/download`;
return (
// eslint-disable-next-line fabmanager/component-class-named-as-component
<a href={link} target="_blank" className="download-button" rel="noreferrer">
<i className="fas fa-download" />
{t('app.shared.schedules_table.download')}
{t('app.shared.payment_schedules_table.download')}
</a>
);
};
@ -104,11 +107,12 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
* Return the human-readable string for the status of the provided deadline.
*/
const formatState = (item: PaymentScheduleItem, schedule: PaymentSchedule): JSX.Element => {
let res = t(`app.shared.schedules_table.state_${item.state}${item.state === 'pending' ? '_' + schedule.payment_method : ''}`);
let res = t(`app.shared.payment_schedules_table.state_${item.state}${item.state === 'pending' ? '_' + schedule.payment_method : ''}`);
if (item.state === PaymentScheduleItemState.Paid) {
const key = `app.shared.schedules_table.method_${item.payment_method}`;
res += ` (${t(key)})`;
}
// eslint-disable-next-line fabmanager/component-class-named-as-component
return <span className={`state-${item.state}`}>{res}</span>;
};
@ -119,16 +123,19 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
refreshList();
};
/**
* Return the JSX table element that list all payment schedules and allows to perform actions on them.
*/
const renderPaymentSchedulesTable = (): ReactElement => {
return (
<table className="schedules-table">
<table className="payment-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">{t('app.shared.payment_schedules_table.schedule_num')}</th>
<th className="w-200">{t('app.shared.payment_schedules_table.date')}</th>
<th className="w-120">{t('app.shared.payment_schedules_table.price')}</th>
{showCustomer && <th className="w-200">{t('app.shared.payment_schedules_table.customer')}</th>}
<th className="w-200"/>
</tr>
</thead>
@ -152,9 +159,9 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
<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-120">{t('app.shared.payment_schedules_table.deadline')}</th>
<th className="w-120">{t('app.shared.payment_schedules_table.amount')}</th>
<th className="w-200">{t('app.shared.payment_schedules_table.state')}</th>
<th className="w-200" />
</tr>
</thead>
@ -212,12 +219,14 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
return <div />;
}
};
PaymentSchedulesTableComponent.defaultProps = { showCustomer: false };
PaymentSchedulesTable.defaultProps = { showCustomer: false };
export const PaymentSchedulesTable: React.FC<PaymentSchedulesTableProps> = ({ paymentSchedules, showCustomer, refreshList, operator, onError, onCardUpdateSuccess }) => {
const PaymentSchedulesTableWrapper: React.FC<PaymentSchedulesTableProps> = ({ paymentSchedules, showCustomer, refreshList, operator, onError, onCardUpdateSuccess }) => {
return (
<Loader>
<PaymentSchedulesTableComponent paymentSchedules={paymentSchedules} showCustomer={showCustomer} refreshList={refreshList} operator={operator} onError={onError} onCardUpdateSuccess={onCardUpdateSuccess} />
<PaymentSchedulesTable paymentSchedules={paymentSchedules} showCustomer={showCustomer} refreshList={refreshList} operator={operator} onError={onError} onCardUpdateSuccess={onCardUpdateSuccess} />
</Loader>
);
};
export { PaymentSchedulesTableWrapper as PaymentSchedulesTable };

View File

@ -25,7 +25,7 @@ export const SelectSchedule: React.FC<SelectScheduleProps> = ({ show, selected,
return (
<div className="select-schedule">
{show && <div className={className || ''}>
<label htmlFor="payment_schedule">{ t('app.shared.cart.monthly_payment') }</label>
<label htmlFor="payment_schedule">{ t('app.shared.select_schedule.monthly_payment') }</label>
<Switch checked={selected} id="payment_schedule" onChange={onChange} className="schedule-switch" />
</div>}
</div>

View File

@ -19,6 +19,9 @@ interface UpdatePaymentMeanModalProps {
*/
type selectOption = { value: PaymentMethod, label: string };
/**
* Component to allow the member to change his payment mean for the given payment schedule (e.g. from card to transfer)
*/
export const UpdatePaymentMeanModal: React.FC<UpdatePaymentMeanModalProps> = ({ isOpen, toggleModal, onError, afterSuccess, paymentSchedule }) => {
const { t } = useTranslation('admin');

View File

@ -1,7 +1,7 @@
import React, { FunctionComponent, ReactNode, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import WalletLib from '../../lib/wallet';
import { WalletInfo } from '../wallet-info';
import { WalletInfo } from './wallet-info';
import { FabModal, ModalSize } from '../base/fab-modal';
import { HtmlTranslate } from '../base/html-translate';
import { CustomAsset, CustomAssetName } from '../../models/custom-asset';
@ -220,13 +220,13 @@ export const AbstractPaymentModal: React.FC<AbstractPaymentModalProps> = ({ isOp
{errors}
</div>}
{hasPaymentScheduleInfo() && <div className="payment-schedule-info">
<HtmlTranslate trKey="app.shared.payment.payment_schedule_html" options={{ DEADLINES: `${schedule.items.length}`, GATEWAY: gateway }} />
<HtmlTranslate trKey="app.shared.abstract_payment_modal.payment_schedule_html" options={{ DEADLINES: `${schedule.items.length}`, GATEWAY: gateway }} />
</div>}
{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_') }
<label htmlFor="acceptToS">{ t('app.shared.abstract_payment_modal.i_have_read_and_accept_') }
<a href={cgv.custom_asset_file_attributes.attachment_url} target="_blank" rel="noreferrer">
{ t('app.shared.payment._the_general_terms_and_conditions') }
{ t('app.shared.abstract_payment_modal._the_general_terms_and_conditions') }
</a>
</label>
</div>}
@ -235,8 +235,8 @@ export const AbstractPaymentModal: React.FC<AbstractPaymentModalProps> = ({ isOp
disabled={!canSubmit()}
form={formId}
className="validate-btn">
{remainingPrice > 0 && t('app.shared.payment.confirm_payment_of_', { AMOUNT: FormatLib.price(remainingPrice) })}
{remainingPrice === 0 && t('app.shared.payment.validate')}
{remainingPrice > 0 && t('app.shared.abstract_payment_modal.confirm_payment_of_', { AMOUNT: FormatLib.price(remainingPrice) })}
{remainingPrice === 0 && t('app.shared.abstract_payment_modal.validate')}
</button>}
{submitState && <div className="payment-pending">
<div className="fa-2x">

View File

@ -2,7 +2,7 @@ import React, { ReactElement, useEffect, useState } from 'react';
import { react2angular } from 'react2angular';
import { Loader } from '../base/loader';
import { StripeModal } from './stripe/stripe-modal';
import { PayZenModal } from './payzen/payzen-modal';
import { PayzenModal } from './payzen/payzen-modal';
import { IApplication } from '../../models/application';
import { ShoppingCart } from '../../models/payment';
import { User } from '../../models/user';
@ -29,7 +29,7 @@ interface CardPaymentModalProps {
* 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 CardPaymentModalComponent: React.FC<CardPaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, currentUser, schedule, cart, customer }) => {
const CardPaymentModal: React.FC<CardPaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, currentUser, schedule, cart, customer }) => {
const { t } = useTranslation('shared');
const [gateway, setGateway] = useState<Setting>(null);
@ -58,7 +58,7 @@ const CardPaymentModalComponent: React.FC<CardPaymentModalProps> = ({ isOpen, to
* Render the PayZen payment modal
*/
const renderPayZenModal = (): ReactElement => {
return <PayZenModal isOpen={isOpen}
return <PayzenModal isOpen={isOpen}
toggleModal={toggleModal}
afterSuccess={afterSuccess}
onError={onError}
@ -80,21 +80,23 @@ const CardPaymentModalComponent: React.FC<CardPaymentModalProps> = ({ isOpen, to
return renderPayZenModal();
case null:
case undefined:
onError(t('app.shared.payment_modal.online_payment_disabled'));
onError(t('app.shared.card_payment_modal.online_payment_disabled'));
return <div />;
default:
onError(t('app.shared.payment_modal.unexpected_error'));
onError(t('app.shared.card_payment_modal.unexpected_error'));
console.error(`[PaymentModal] Unimplemented gateway: ${gateway.value}`);
return <div />;
}
};
export const CardPaymentModal: React.FC<CardPaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, currentUser, schedule, cart, customer }) => {
const CardPaymentModalWrapper: React.FC<CardPaymentModalProps> = (props) => {
return (
<Loader>
<CardPaymentModalComponent isOpen={isOpen} toggleModal={toggleModal} afterSuccess={afterSuccess} onError={onError} currentUser={currentUser} schedule={schedule} cart={cart} customer={customer} />
<CardPaymentModal {...props} />
</Loader>
);
};
Application.Components.component('cardPaymentModal', react2angular(CardPaymentModal, ['isOpen', 'toggleModal', 'afterSuccess', 'onError', 'currentUser', 'schedule', 'cart', 'customer']));
export { CardPaymentModalWrapper as CardPaymentModal };
Application.Components.component('cardPaymentModal', react2angular(CardPaymentModalWrapper, ['isOpen', 'toggleModal', 'afterSuccess', 'onError', 'currentUser', 'schedule', 'cart', 'customer']));

View File

@ -54,7 +54,7 @@ export const LocalPaymentForm: React.FC<GatewayFormProps> = ({ onSubmit, onSucce
const methodToOption = (value: scheduleMethod): selectOption => {
if (!value) return { value, label: '' };
return { value, label: t(`app.admin.local_payment.method_${value}`) };
return { value, label: t(`app.admin.local_payment_form.method_${value}`) };
};
/**
@ -77,7 +77,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_form.online_payment_disabled'));
}
return toggleOnlinePaymentModal();
} catch (e) {
@ -118,21 +118,21 @@ export const LocalPaymentForm: React.FC<GatewayFormProps> = ({ onSubmit, onSucce
};
return (
<form onSubmit={handleSubmit} id={formId} className={className || ''}>
{!paymentSchedule && !isFreeOfCharge() && <p className="payment">{t('app.admin.local_payment.about_to_cash')}</p>}
{!paymentSchedule && isFreeOfCharge() && <p className="payment">{t('app.admin.local_payment.about_to_confirm', { ITEM: mainItemType() })}</p>}
<form onSubmit={handleSubmit} id={formId} className={`local-payment-form ${className || ''}`}>
{!paymentSchedule && !isFreeOfCharge() && <p className="payment">{t('app.admin.local_payment_form.about_to_cash')}</p>}
{!paymentSchedule && isFreeOfCharge() && <p className="payment">{t('app.admin.local_payment_form.about_to_confirm', { ITEM: mainItemType() })}</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') }
<label htmlFor="payment-method">{t('app.admin.local_payment_form.payment_method')}</label>
<Select placeholder={ t('app.admin.local_payment_form.payment_method') }
id="payment-method"
className="method-select"
onChange={handleUpdateMethod}
options={buildMethodOptions()}
value={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>}
{method === 'transfer' && <HtmlTranslate trKey="app.admin.local_payment.transfer_collection_info" options={{ DEADLINES: paymentSchedule.items.length }} />}
{method === 'card' && <p>{t('app.admin.local_payment_form.card_collection_info')}</p>}
{method === 'check' && <p>{t('app.admin.local_payment_form.check_collection_info', { DEADLINES: paymentSchedule.items.length })}</p>}
{method === 'transfer' && <HtmlTranslate trKey="app.admin.local_payment_form.transfer_collection_info" options={{ DEADLINES: paymentSchedule.items.length }} />}
</div>
<div className="full-schedule">
<ul>

View File

@ -28,7 +28,7 @@ interface LocalPaymentModalProps {
/**
* This component enables a privileged user to confirm a local payments.
*/
const LocalPaymentModalComponent: React.FC<LocalPaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, cart, updateCart, currentUser, schedule, customer }) => {
const LocalPaymentModal: React.FC<LocalPaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, cart, updateCart, currentUser, schedule, customer }) => {
const { t } = useTranslation('admin');
/**
@ -76,7 +76,7 @@ const LocalPaymentModalComponent: React.FC<LocalPaymentModalProps> = ({ isOpen,
isOpen={isOpen}
toggleModal={toggleModal}
logoFooter={logoFooter()}
title={isFreeOfCharge() ? t('app.admin.local_payment.validate_cart') : t('app.admin.local_payment.offline_payment')}
title={isFreeOfCharge() ? t('app.admin.local_payment_modal.validate_cart') : t('app.admin.local_payment_modal.offline_payment')}
formId="local-payment-form"
formClassName="local-payment-form"
currentUser={currentUser}
@ -93,12 +93,14 @@ const LocalPaymentModalComponent: React.FC<LocalPaymentModalProps> = ({ isOpen,
);
};
export const LocalPaymentModal: React.FC<LocalPaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, currentUser, schedule, cart, updateCart, customer }) => {
const LocalPaymentModalWrapper: React.FC<LocalPaymentModalProps> = (props) => {
return (
<Loader>
<LocalPaymentModalComponent isOpen={isOpen} toggleModal={toggleModal} afterSuccess={afterSuccess} onError={onError} currentUser={currentUser} schedule={schedule} cart={cart} updateCart={updateCart} customer={customer} />
<LocalPaymentModal {...props} />
</Loader>
);
};
Application.Components.component('localPaymentModal', react2angular(LocalPaymentModal, ['isOpen', 'toggleModal', 'afterSuccess', 'onError', 'currentUser', 'schedule', 'cart', 'updateCart', 'customer']));
export { LocalPaymentModalWrapper as LocalPaymentModal };
Application.Components.component('localPaymentModal', react2angular(LocalPaymentModalWrapper, ['isOpen', 'toggleModal', 'afterSuccess', 'onError', 'currentUser', 'schedule', 'cart', 'updateCart', 'customer']));

View File

@ -16,6 +16,9 @@ interface PayzenCardUpdateModalProps {
operator: User
}
/**
* Modal dialog to allow the member to update his payment card for a payment schedule, when the PayZen gateway is used
*/
export const PayzenCardUpdateModal: React.FC<PayzenCardUpdateModalProps> = ({ isOpen, toggleModal, onSuccess, schedule, operator }) => {
const { t } = useTranslation('shared');
@ -61,7 +64,7 @@ export const PayzenCardUpdateModal: React.FC<PayzenCardUpdateModalProps> = ({ is
toggleModal={toggleModal}
closeButton={false}
customFooter={logoFooter()}
className="payzen-update-card-modal">
className="payzen-card-update-modal">
{schedule && <PayzenForm onSubmit={handleCardUpdateSubmit}
onSuccess={onSuccess}
onError={handleCardUpdateError}

View File

@ -143,6 +143,9 @@ export const PayzenForm: React.FC<PayzenFormProps> = ({ onSubmit, onSuccess, onE
}
};
/**
* Return a loader
*/
const Loader: FunctionComponent = () => {
return (
<div className={`fa-3x ${loadingClass}`}>
@ -152,9 +155,9 @@ export const PayzenForm: React.FC<PayzenFormProps> = ({ onSubmit, onSuccess, onE
};
return (
<form onSubmit={handleSubmit} id={formId} className={className || ''}>
<form onSubmit={handleSubmit} id={formId} className={`payzen-form ${className || ''}`}>
<Loader />
<div className="container">
<div className="payzen-container">
<div id="payzenPaymentForm" />
</div>
{children}

View File

@ -11,7 +11,7 @@ import PayzenAPI from '../../../api/payzen';
enableMapSet();
interface PayZenKeysFormProps {
interface PayzenKeysFormProps {
onValidKeys: (payZenSettings: Map<SettingName, string>) => void,
onInvalidKeys: () => void,
}
@ -28,7 +28,7 @@ let pendingKeysValidation = false;
/**
* Form to set the PayZen's username, password and public key
*/
const PayZenKeysFormComponent: React.FC<PayZenKeysFormProps> = ({ onValidKeys, onInvalidKeys }) => {
const PayzenKeysForm: React.FC<PayzenKeysFormProps> = ({ onValidKeys, onInvalidKeys }) => {
const { t } = useTranslation('admin');
// values of the PayZen settings
@ -139,13 +139,13 @@ const PayZenKeysFormComponent: React.FC<PayZenKeysFormProps> = ({ onValidKeys, o
return (
<div className="payzen-keys-form">
<div className="payzen-keys-info">
<HtmlTranslate trKey="app.admin.invoices.payment.payzen_keys_info_html" />
<HtmlTranslate trKey="app.admin.invoices.payzen_keys_form.payzen_keys_info_html" />
</div>
<form name="payzenKeysForm">
<fieldset>
<legend>{t('app.admin.invoices.payment.client_keys')}</legend>
<legend>{t('app.admin.invoices.payzen_keys_form.client_keys')}</legend>
<div className="payzen-public-input">
<label htmlFor="payzen_public_key">{ t('app.admin.invoices.payment.payzen.payzen_public_key') } *</label>
<label htmlFor="payzen_public_key">{ t('app.admin.invoices.payzen_keys_form.payzen_public_key') } *</label>
<FabInput id="payzen_public_key"
icon={<i className="fas fa-info" />}
defaultValue={settings.get(SettingName.PayZenPublicKey)}
@ -158,11 +158,11 @@ const PayZenKeysFormComponent: React.FC<PayZenKeysFormProps> = ({ onValidKeys, o
</fieldset>
<fieldset>
<legend className={hasApiAddOn() ? 'with-addon' : ''}>
<span>{t('app.admin.invoices.payment.api_keys')}</span>
<span>{t('app.admin.invoices.payzen_keys_form.api_keys')}</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>
<label htmlFor="payzen_username">{ t('app.admin.invoices.payzen_keys_form.payzen_username') } *</label>
<FabInput id="payzen_username"
type="number"
icon={<i className="fas fa-user-alt" />}
@ -172,7 +172,7 @@ const PayZenKeysFormComponent: React.FC<PayZenKeysFormProps> = ({ onValidKeys, o
required />
</div>
<div className="payzen-api-password-input">
<label htmlFor="payzen_password">{ t('app.admin.invoices.payment.payzen.payzen_password') } *</label>
<label htmlFor="payzen_password">{ t('app.admin.invoices.payzen_keys_form.payzen_password') } *</label>
<FabInput id="payzen_password"
icon={<i className="fas fa-key" />}
defaultValue={settings.get(SettingName.PayZenPassword)}
@ -181,7 +181,7 @@ const PayZenKeysFormComponent: React.FC<PayZenKeysFormProps> = ({ onValidKeys, o
required />
</div>
<div className="payzen-api-endpoint-input">
<label htmlFor="payzen_endpoint">{ t('app.admin.invoices.payment.payzen.payzen_endpoint') } *</label>
<label htmlFor="payzen_endpoint">{ t('app.admin.invoices.payzen_keys_form.payzen_endpoint') } *</label>
<FabInput id="payzen_endpoint"
type="url"
icon={<i className="fas fa-link" />}
@ -191,7 +191,7 @@ const PayZenKeysFormComponent: React.FC<PayZenKeysFormProps> = ({ onValidKeys, o
required />
</div>
<div className="payzen-api-hmac-input">
<label htmlFor="payzen_hmac">{ t('app.admin.invoices.payment.payzen.payzen_hmac') } *</label>
<label htmlFor="payzen_hmac">{ t('app.admin.invoices.payzen_keys_form.payzen_hmac') } *</label>
<FabInput id="payzen_hmac"
icon={<i className="fas fa-subscript" />}
defaultValue={settings.get(SettingName.PayZenHmacKey)}
@ -205,10 +205,12 @@ const PayZenKeysFormComponent: React.FC<PayZenKeysFormProps> = ({ onValidKeys, o
);
};
export const PayZenKeysForm: React.FC<PayZenKeysFormProps> = ({ onValidKeys, onInvalidKeys }) => {
const PayzenKeysFormWrapper: React.FC<PayzenKeysFormProps> = (props) => {
return (
<Loader>
<PayZenKeysFormComponent onValidKeys={onValidKeys} onInvalidKeys={onInvalidKeys} />
<PayzenKeysForm {...props} />
</Loader>
);
};
export { PayzenKeysFormWrapper as PayzenKeysForm };

View File

@ -10,7 +10,7 @@ import mastercardLogo from '../../../../../images/mastercard.png';
import visaLogo from '../../../../../images/visa.png';
import { PayzenForm } from './payzen-form';
interface PayZenModalProps {
interface PayzenModalProps {
isOpen: boolean,
toggleModal: () => void,
afterSuccess: (result: Invoice|PaymentSchedule) => void,
@ -28,7 +28,7 @@ interface PayZenModalProps {
* This component should not be called directly. Prefer using <CardPaymentModal> which can handle the configuration
* of a different payment gateway.
*/
export const PayZenModal: React.FC<PayZenModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, cart, currentUser, schedule, customer }) => {
export const PayzenModal: React.FC<PayzenModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, cart, currentUser, schedule, customer }) => {
/**
* Return the logos, shown in the modal footer.
*/

View File

@ -83,7 +83,7 @@ export const PayzenSettings: React.FC<PayzenSettingsProps> = ({ onEditKeys, onCu
setError('');
updateSettings(draft => draft.set(SettingName.PayZenCurrency, value));
} else {
setError(t('app.admin.invoices.payment.payzen.currency_error'));
setError(t('app.admin.invoices.payment.payzen_settings.currency_error'));
}
};
@ -97,18 +97,18 @@ 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_settings.error_while_saving') + reason);
});
};
return (
<div className="payzen-settings">
<h3 className="title">{t('app.admin.invoices.payment.payzen.payzen_keys')}</h3>
<h3 className="title">{t('app.admin.invoices.payment.payzen_settings.payzen_keys')}</h3>
<div className="payzen-keys">
{payZenPublicSettings.concat(payZenPrivateSettings).map(setting => {
return (
<div className="key-wrapper" key={setting}>
<label htmlFor={setting}>{t(`app.admin.invoices.payment.payzen.${setting}`)}</label>
<label htmlFor={setting}>{t(`app.admin.invoices.payment.payzen_settings.${setting}`)}</label>
<FabInput defaultValue={settings.get(setting)}
id={setting}
type={payZenPrivateSettings.indexOf(setting) > -1 ? 'password' : 'text'}
@ -119,17 +119,17 @@ export const PayzenSettings: React.FC<PayzenSettingsProps> = ({ onEditKeys, onCu
);
})}
<div className="edit-keys">
<FabButton className="edit-keys-btn" onClick={handleKeysUpdate}>{t('app.admin.invoices.payment.edit_keys')}</FabButton>
<FabButton className="edit-keys-btn" onClick={handleKeysUpdate}>{t('app.admin.invoices.payment.payzen_settings.edit_keys')}</FabButton>
</div>
</div>
<div className="payzen-currency">
<h3 className="title">{t('app.admin.invoices.payment.payzen.currency')}</h3>
<h3 className="title">{t('app.admin.invoices.payment.payzen_settings.currency')}</h3>
<p className="currency-info">
<HtmlTranslate trKey="app.admin.invoices.payment.payzen.currency_info_html" />
<HtmlTranslate trKey="app.admin.invoices.payment.payzen_settings.currency_info_html" />
</p>
<div className="payzen-currency-form">
<div className="currency-wrapper">
<label htmlFor="payzen_currency">{t('app.admin.invoices.payment.payzen.payzen_currency')}</label>
<label htmlFor="payzen_currency">{t('app.admin.invoices.payment.payzen_settings.payzen_currency')}</label>
<FabInput defaultValue={settings.get(SettingName.PayZenCurrency)}
id="payzen_currency"
icon={<i className="fas fa-money-bill" />}
@ -138,7 +138,7 @@ export const PayzenSettings: React.FC<PayzenSettingsProps> = ({ onEditKeys, onCu
pattern="[A-Z]{3}"
error={error} />
</div>
<FabButton className="save-currency" onClick={saveCurrency}>{t('app.admin.invoices.payment.payzen.save')}</FabButton>
<FabButton className="save-currency" onClick={saveCurrency}>{t('app.admin.invoices.payment.payzen_settings.save')}</FabButton>
</div>
</div>
</div>

View File

@ -6,15 +6,15 @@
import React, { BaseSyntheticEvent, useEffect, useState } from 'react';
import { react2angular } from 'react2angular';
import { useTranslation } from 'react-i18next';
import { StripeKeysForm } from './payment/stripe/stripe-keys-form';
import { PayZenKeysForm } from './payment/payzen/payzen-keys-form';
import { FabModal, ModalSize } from './base/fab-modal';
import { Loader } from './base/loader';
import { User } from '../models/user';
import { Gateway } from '../models/gateway';
import { SettingBulkResult, SettingName } from '../models/setting';
import { IApplication } from '../models/application';
import SettingAPI from '../api/setting';
import { StripeKeysForm } from './stripe/stripe-keys-form';
import { PayzenKeysForm } from './payzen/payzen-keys-form';
import { FabModal, ModalSize } from '../base/fab-modal';
import { Loader } from '../base/loader';
import { User } from '../../models/user';
import { Gateway } from '../../models/gateway';
import { SettingBulkResult, SettingName } from '../../models/setting';
import { IApplication } from '../../models/application';
import SettingAPI from '../../api/setting';
declare const Application: IApplication;
@ -26,7 +26,10 @@ interface SelectGatewayModalModalProps {
onSuccess: (results: Map<SettingName, SettingBulkResult>) => void,
}
const SelectGatewayModal: React.FC<SelectGatewayModalModalProps> = ({ isOpen, toggleModal, onError, onSuccess }) => {
/**
* Modal dialog that enable an admin to configure the active payment gateway
*/
export const SelectGatewayModal: React.FC<SelectGatewayModalModalProps> = ({ isOpen, toggleModal, onError, onSuccess }) => {
const { t } = useTranslation('admin');
const [preventConfirmGateway, setPreventConfirmGateway] = useState<boolean>(true);
@ -113,33 +116,33 @@ const SelectGatewayModal: React.FC<SelectGatewayModalModalProps> = ({ isOpen, to
};
return (
<FabModal title={t('app.admin.invoices.payment.gateway_modal.select_gateway_title')}
<FabModal title={t('app.admin.invoices.payment.select_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')}
className="select-gateway-modal"
confirmButton={t('app.admin.invoices.payment.select_gateway_modal.confirm_button')}
onConfirm={onGatewayConfirmed}
preventConfirm={preventConfirmGateway}>
{!hasSelectedGateway() && <p className="info-gateway">
{t('app.admin.invoices.payment.gateway_modal.gateway_info')}
{t('app.admin.invoices.payment.select_gateway_modal.gateway_info')}
</p>}
<label htmlFor="gateway">{t('app.admin.invoices.payment.gateway_modal.select_gateway')}</label>
<label htmlFor="gateway">{t('app.admin.invoices.payment.select_gateway_modal.select_gateway')}</label>
<select id="gateway" className="select-gateway" onChange={setGateway} value={selectedGateway}>
<option />
<option value={Gateway.Stripe}>{t('app.admin.invoices.payment.gateway_modal.stripe')}</option>
<option value={Gateway.PayZen}>{t('app.admin.invoices.payment.gateway_modal.payzen')}</option>
<option value={Gateway.Stripe}>{t('app.admin.invoices.payment.select_gateway_modal.stripe')}</option>
<option value={Gateway.PayZen}>{t('app.admin.invoices.payment.select_gateway_modal.payzen')}</option>
</select>
{selectedGateway === Gateway.Stripe && <StripeKeysForm onValidKeys={handleValidStripeKeys} onInvalidKeys={handleInvalidKeys} />}
{selectedGateway === Gateway.PayZen && <PayZenKeysForm onValidKeys={handleValidPayZenKeys} onInvalidKeys={handleInvalidKeys} />}
{selectedGateway === Gateway.PayZen && <PayzenKeysForm onValidKeys={handleValidPayZenKeys} onInvalidKeys={handleInvalidKeys} />}
</FabModal>
);
};
const SelectGatewayModalWrapper: React.FC<SelectGatewayModalModalProps> = ({ isOpen, toggleModal, currentUser, onSuccess, onError }) => {
const SelectGatewayModalWrapper: React.FC<SelectGatewayModalModalProps> = (props) => {
return (
<Loader>
<SelectGatewayModal isOpen={isOpen} toggleModal={toggleModal} currentUser={currentUser} onSuccess={onSuccess} onError={onError} />
<SelectGatewayModal {...props} />
</Loader>
);
};

View File

@ -16,6 +16,9 @@ interface StripeCardUpdateModalProps {
operator: User
}
/**
* Modal dialog to allow the member to update his payment card for a payment schedule, when the Stripe gateway is used
*/
export const StripeCardUpdateModal: React.FC<StripeCardUpdateModalProps> = ({ isOpen, toggleModal, onSuccess, schedule, operator }) => {
const { t } = useTranslation('shared');
@ -30,7 +33,7 @@ export const StripeCardUpdateModal: React.FC<StripeCardUpdateModalProps> = ({ is
const logoFooter = (): ReactNode => {
return (
<div className="stripe-modal-icons">
<i className="fa fa-lock fa-2x m-r-sm pos-rlt" />
<i className="fa fa-lock fa-2x" />
<img src={stripeLogo} alt="powered by stripe" />
<img src={mastercardLogo} alt="mastercard" />
<img src={visaLogo} alt="visa" />
@ -59,7 +62,7 @@ export const StripeCardUpdateModal: React.FC<StripeCardUpdateModalProps> = ({ is
toggleModal={toggleModal}
closeButton={false}
customFooter={logoFooter()}
className="stripe-update-card-modal">
className="stripe-card-update-modal">
{schedule && <StripeCardUpdate onSubmit={handleCardUpdateSubmit}
onSuccess={onSuccess}
onError={handleCardUpdateError}

View File

@ -95,7 +95,7 @@ export const StripeCardUpdate: React.FC<StripeCardUpdateProps> = ({ onSubmit, on
};
return (
<form onSubmit={handleSubmit} id="stripe-card" className={className}>
<form onSubmit={handleSubmit} id="stripe-card" className={`stripe-card-update ${className}`}>
<CardElement options={cardOptions} />
{children}
</form>

View File

@ -43,11 +43,11 @@ export const StripeConfirmModal: React.FC<StripeConfirmModalProps> = ({ isOpen,
};
return (
<FabModal title={t('app.shared.schedules_table.resolve_action')}
<FabModal title={t('app.shared.stripe_confirm_modal.resolve_action')}
isOpen={isOpen}
toggleModal={toggleModal}
onConfirm={onConfirmed}
confirmButton={t('app.shared.schedules_table.ok_button')}
confirmButton={t('app.shared.stripe_confirm_modal.ok_button')}
preventConfirm={isPending}>
{item && <StripeConfirm clientSecret={item.client_secret} onResponse={togglePending} />}
</FabModal>

View File

@ -66,7 +66,7 @@ export const StripeForm: React.FC<GatewayFormProps> = ({ onSubmit, onSuccess, on
if (response.error.statusText) {
onError(response.error.statusText);
} else {
onError(`${t('app.shared.messages.payment_card_error')} ${response.error}`);
onError(`${t('app.shared.stripe_form.payment_card_error')} ${response.error}`);
}
} else if ('requires_action' in response) {
if (response.type === 'payment') {
@ -125,7 +125,7 @@ export const StripeForm: React.FC<GatewayFormProps> = ({ onSubmit, onSuccess, on
};
return (
<form onSubmit={handleSubmit} id={formId} className={className || ''}>
<form onSubmit={handleSubmit} id={formId} className={`stripe-form ${className || ''}`}>
<CardElement options={cardOptions} />
{children}
</form>

View File

@ -15,7 +15,7 @@ interface StripeKeysFormProps {
/**
* Form to set the stripe's public and private keys
*/
const StripeKeysFormComponent: React.FC<StripeKeysFormProps> = ({ onValidKeys, onInvalidKeys }) => {
const StripeKeysForm: React.FC<StripeKeysFormProps> = ({ onValidKeys, onInvalidKeys }) => {
const { t } = useTranslation('admin');
// used to prevent promises from resolving if the component was unmounted
@ -123,11 +123,11 @@ const StripeKeysFormComponent: React.FC<StripeKeysFormProps> = ({ onValidKeys, o
return (
<div className="stripe-keys-form">
<div className="stripe-keys-info">
<HtmlTranslate trKey="app.admin.invoices.payment.stripe_keys_info_html" />
<HtmlTranslate trKey="app.admin.invoices.stripe_keys_form.stripe_keys_info_html" />
</div>
<form name="stripeKeysForm">
<div className="stripe-public-input">
<label htmlFor="stripe_public_key">{ t('app.admin.invoices.payment.public_key') } *</label>
<label htmlFor="stripe_public_key">{ t('app.admin.invoices.stripe_keys_form.public_key') } *</label>
<FabInput id="stripe_public_key"
icon={<i className="fa fa-info" />}
defaultValue={publicKey}
@ -138,7 +138,7 @@ const StripeKeysFormComponent: React.FC<StripeKeysFormProps> = ({ onValidKeys, o
required />
</div>
<div className="stripe-secret-input">
<label htmlFor="stripe_secret_key">{ t('app.admin.invoices.payment.secret_key') } *</label>
<label htmlFor="stripe_secret_key">{ t('app.admin.invoices.stripe_keys_form.secret_key') } *</label>
<FabInput id="stripe_secret_key"
icon={<i className="fa fa-key" />}
defaultValue={secretKey}
@ -153,10 +153,12 @@ const StripeKeysFormComponent: React.FC<StripeKeysFormProps> = ({ onValidKeys, o
);
};
export const StripeKeysForm: React.FC<StripeKeysFormProps> = ({ onValidKeys, onInvalidKeys }) => {
const StripeKeysFormWrapper: React.FC<StripeKeysFormProps> = (props) => {
return (
<Loader>
<StripeKeysFormComponent onValidKeys={onValidKeys} onInvalidKeys={onInvalidKeys} />
<StripeKeysForm {...props} />
</Loader>
);
};
export { StripeKeysFormWrapper as StripeKeysForm };

View File

@ -36,7 +36,7 @@ export const StripeModal: React.FC<StripeModalProps> = ({ isOpen, toggleModal, a
const logoFooter = (): ReactNode => {
return (
<div className="stripe-modal-icons">
<i className="fa fa-lock fa-2x m-r-sm pos-rlt" />
<i className="fa fa-lock fa-2x" />
<img src={stripeLogo} alt="powered by stripe" />
<img src={mastercardLogo} alt="mastercard" />
<img src={visaLogo} alt="visa" />

View File

@ -19,7 +19,7 @@ interface UpdateCardModalProps {
* 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 UpdateCardModalComponent: React.FC<UpdateCardModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, operator, schedule }) => {
const UpdateCardModal: React.FC<UpdateCardModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, operator, schedule }) => {
const { t } = useTranslation('shared');
const [gateway, setGateway] = useState<string>('');
@ -68,10 +68,12 @@ const UpdateCardModalComponent: React.FC<UpdateCardModalProps> = ({ isOpen, togg
}
};
export const UpdateCardModal: React.FC<UpdateCardModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, operator, schedule }) => {
const UpdateCardModalWrapper: React.FC<UpdateCardModalProps> = (props) => {
return (
<Loader>
<UpdateCardModalComponent isOpen={isOpen} toggleModal={toggleModal} afterSuccess={afterSuccess} onError={onError} operator={operator} schedule={schedule} />
<UpdateCardModal {...props} />
</Loader>
);
};
export { UpdateCardModalWrapper as UpdateCardModal };

View File

@ -1,14 +1,14 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { react2angular } from 'react2angular';
import { IApplication } from '../models/application';
import '../lib/i18n';
import { Loader } from './base/loader';
import { User } from '../models/user';
import { Wallet } from '../models/wallet';
import WalletLib from '../lib/wallet';
import { ShoppingCart } from '../models/payment';
import FormatLib from '../lib/format';
import { IApplication } from '../../models/application';
import '../../lib/i18n';
import { Loader } from '../base/loader';
import { User } from '../../models/user';
import { Wallet } from '../../models/wallet';
import WalletLib from '../../lib/wallet';
import { ShoppingCart } from '../../models/payment';
import FormatLib from '../../lib/format';
declare const Application: IApplication;

View File

@ -16,7 +16,7 @@ interface DeletePlanCategoryProps {
* This component shows a button.
* When clicked, we show a modal dialog to ask the user for confirmation about the deletion of the provided plan-category.
*/
const DeletePlanCategoryComponent: React.FC<DeletePlanCategoryProps> = ({ onSuccess, onError, category }) => {
const DeletePlanCategory: React.FC<DeletePlanCategoryProps> = ({ onSuccess, onError, category }) => {
const { t } = useTranslation('admin');
const [deletionModal, setDeletionModal] = useState<boolean>(false);
@ -34,9 +34,9 @@ const DeletePlanCategoryComponent: React.FC<DeletePlanCategoryProps> = ({ onSucc
*/
const onDeleteConfirmed = (): void => {
PlanCategoryAPI.destroy(category.id).then(() => {
onSuccess(t('app.admin.manage_plan_category.delete_category.success'));
onSuccess(t('app.admin.delete_plan_category.success'));
}).catch((error) => {
onError(t('app.admin.manage_plan_category.delete_category.error') + error);
onError(t('app.admin.delete_plan_category.error') + error);
});
toggleDeletionModal();
};
@ -44,22 +44,24 @@ const DeletePlanCategoryComponent: React.FC<DeletePlanCategoryProps> = ({ onSucc
return (
<div className="delete-plan-category">
<FabButton type='button' className="delete-button" icon={<i className="fa fa-trash" />} onClick={toggleDeletionModal} />
<FabModal title={t('app.admin.manage_plan_category.delete_category.title')}
<FabModal title={t('app.admin.delete_plan_category.title')}
isOpen={deletionModal}
toggleModal={toggleDeletionModal}
closeButton={true}
confirmButton={t('app.admin.manage_plan_category.delete_category.cta')}
confirmButton={t('app.admin.delete_plan_category.cta')}
onConfirm={onDeleteConfirmed}>
<span>{t('app.admin.manage_plan_category.delete_category.confirm')}</span>
<span>{t('app.admin.delete_plan_category.confirm')}</span>
</FabModal>
</div>
);
};
export const DeletePlanCategory: React.FC<DeletePlanCategoryProps> = ({ onSuccess, onError, category }) => {
const DeletePlanCategoryWrapper: React.FC<DeletePlanCategoryProps> = (props) => {
return (
<Loader>
<DeletePlanCategoryComponent onSuccess={onSuccess} onError={onError} category={category} />
<DeletePlanCategory {...props} />
</Loader>
);
};
export { DeletePlanCategoryWrapper as DeletePlanCategory };

View File

@ -17,7 +17,7 @@ interface ManagePlanCategoryProps {
* This component shows a button.
* When clicked, we show a modal dialog allowing to fill the parameters of a plan-category (create new or update existing).
*/
const ManagePlanCategoryComponent: React.FC<ManagePlanCategoryProps> = ({ category, action, onSuccess, onError }) => {
const ManagePlanCategory: React.FC<ManagePlanCategoryProps> = ({ category, action, onSuccess, onError }) => {
const { t } = useTranslation('admin');
// is the creation modal open?
@ -61,9 +61,9 @@ const ManagePlanCategoryComponent: React.FC<ManagePlanCategoryProps> = ({ catego
return (
<FabButton type='button'
icon={<i className='fa fa-plus' />}
className="btn-warning"
className="create-button"
onClick={toggleModal}>
{t('app.admin.manage_plan_category.create_category.title')}
{t('app.admin.manage_plan_category.create')}
</FabButton>
);
case 'update':
@ -77,7 +77,7 @@ const ManagePlanCategoryComponent: React.FC<ManagePlanCategoryProps> = ({ catego
return (
<div className='manage-plan-category'>
{ toggleBtn() }
<FabModal title={t(`app.admin.manage_plan_category.${action}_category.title`)}
<FabModal title={t(`app.admin.manage_plan_category.${action}`)}
isOpen={isOpen}
toggleModal={toggleModal}
onCreation={initCategoryCreation}
@ -90,10 +90,12 @@ const ManagePlanCategoryComponent: React.FC<ManagePlanCategoryProps> = ({ catego
);
};
export const ManagePlanCategory: React.FC<ManagePlanCategoryProps> = ({ category, action, onSuccess, onError }) => {
const ManagePlanCategoryWrapper: React.FC<ManagePlanCategoryProps> = (props) => {
return (
<Loader>
<ManagePlanCategoryComponent category={category} action={action} onSuccess={onSuccess} onError={onError} />
<ManagePlanCategory {...props} />
</Loader>
);
};
export { ManagePlanCategoryWrapper as ManagePlanCategory };

View File

@ -16,7 +16,10 @@ interface PlanCategoryFormProps {
onError: (message: string) => void
}
const PlanCategoryFormComponent: React.FC<PlanCategoryFormProps> = ({ action, category, onSuccess, onError }) => {
/**
* Form to create/edit a plan category
*/
const PlanCategoryForm: React.FC<PlanCategoryFormProps> = ({ action, category, onSuccess, onError }) => {
const { t } = useTranslation('admin');
const { register, control, handleSubmit } = useForm<PlanCategory>({ defaultValues: { ...category } });
@ -28,16 +31,16 @@ const PlanCategoryFormComponent: React.FC<PlanCategoryFormProps> = ({ action, ca
switch (action) {
case 'create':
PlanCategoryAPI.create(data).then(() => {
onSuccess(t('app.admin.manage_plan_category.create_category.success'));
onSuccess(t('app.admin.plan_category_form.create.success'));
}).catch((error) => {
onError(t('app.admin.manage_plan_category.create_category.error') + error);
onError(t('app.admin.plan_category_form.create.error') + error);
});
break;
case 'update':
PlanCategoryAPI.update(data).then(() => {
onSuccess(t('app.admin.manage_plan_category.update_category.success'));
onSuccess(t('app.admin.plan_category_form.update.success'));
}).catch((error) => {
onError(t('app.admin.manage_plan_category.update_category.error') + error);
onError(t('app.admin.plan_category_form.update.error') + error);
});
break;
}
@ -45,23 +48,25 @@ const PlanCategoryFormComponent: React.FC<PlanCategoryFormProps> = ({ action, ca
return (
<form onSubmit={handleSubmit(onSubmit)}>
<FormInput id='name' register={register} rules={{ required: 'true' }} label={t('app.admin.manage_plan_category.name')} />
<FormInput id='name' register={register} rules={{ required: 'true' }} label={t('app.admin.plan_category_form.name')} />
<FormRichText control={control} id="description" label={t('app.admin.manage_plan_category.description')} limit={100} />
<FormRichText control={control} id="description" label={t('app.admin.plan_category_form.description')} limit={100} />
<FormInput id='weight' register={register} type='number' label={t('app.admin.manage_plan_category.significance')} />
<FormInput id='weight' register={register} type='number' label={t('app.admin.plan_category_form.significance')} />
<FabAlert level="info" className="significance-info">
{t('app.admin.manage_plan_category.info')}
{t('app.admin.plan_category_form.info')}
</FabAlert>
<FabButton type='submit'>{t(`app.admin.manage_plan_category.${action}_category.cta`)}</FabButton>
<FabButton type='submit'>{t(`app.admin.plan_category_form.${action}.cta`)}</FabButton>
</form>
);
};
export const PlanCategoryForm: React.FC<PlanCategoryFormProps> = ({ action, category, onSuccess, onError }) => {
const PlanCategoryFormWrapper: React.FC<PlanCategoryFormProps> = (props) => {
return (
<Loader>
<PlanCategoryFormComponent action={action} category={category} onSuccess={onSuccess} onError={onError} />
<PlanCategoryForm {...props} />
</Loader>
);
};
export { PlanCategoryFormWrapper as PlanCategoryForm };

View File

@ -22,7 +22,7 @@ interface PlanCardProps {
/**
* This component is a "card" (visually), publicly presenting the details of a plan and allowing a user to subscribe.
*/
const PlanCardComponent: React.FC<PlanCardProps> = ({ plan, userId, subscribedPlanId, operator, onSelectPlan, isSelected, onLoginRequested, canSelectPlan }) => {
const PlanCard: React.FC<PlanCardProps> = ({ plan, userId, subscribedPlanId, operator, onSelectPlan, isSelected, onLoginRequested, canSelectPlan }) => {
const { t } = useTranslation('public');
/**
* Return the formatted localized amount of the given plan (eg. 20.5 => "20,50 €")
@ -105,7 +105,7 @@ const PlanCardComponent: React.FC<PlanCardProps> = ({ plan, userId, subscribedPl
<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.plan_card.AMOUNT_per_month', { AMOUNT: monthlyAmount() })}</div>
<span className="period">{duration()}</span>
</div>
</div>}
@ -118,25 +118,25 @@ const PlanCardComponent: React.FC<PlanCardProps> = ({ plan, userId, subscribedPl
</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" rel="noreferrer">{ t('app.public.plans.more_information') }</a>}
{hasAttachment() && <a className="info-link" href={ plan.plan_file_url } target="_blank" rel="noreferrer">{ t('app.public.plan_card.more_information') }</a>}
{mustLogin() && <div className="cta-button">
<button className="subscribe-button" onClick={handleLoginRequest}>{t('app.public.plans.i_subscribe_online')}</button>
<button className="subscribe-button" onClick={handleLoginRequest}>{t('app.public.plan_card.i_subscribe_online')}</button>
</div>}
{canSubscribeForMe() && <div className="cta-button">
{!hasSubscribedToThisPlan() && <button className={`subscribe-button ${isSelected ? 'selected-card' : ''}`}
onClick={handleSelectPlan}
disabled={!_.isNil(subscribedPlanId)}>
{t('app.public.plans.i_choose_that_plan')}
{t('app.public.plan_card.i_choose_that_plan')}
</button>}
{hasSubscribedToThisPlan() && <button className="subscribe-button selected-card" disabled>
{ t('app.public.plans.i_already_subscribed') }
{ t('app.public.plan_card.i_already_subscribed') }
</button>}
</div>}
{canSubscribeForOther() && <div className="cta-button">
<button className={`subscribe-button ${isSelected ? 'selected-card' : ''}`}
onClick={handleSelectPlan}
disabled={_.isNil(userId)}>
<span>{ t('app.public.plans.i_choose_that_plan') }</span>
<span>{ t('app.public.plan_card.i_choose_that_plan') }</span>
</button>
</div>}
</div>
@ -144,10 +144,12 @@ const PlanCardComponent: React.FC<PlanCardProps> = ({ plan, userId, subscribedPl
);
};
export const PlanCard: React.FC<PlanCardProps> = ({ plan, userId, subscribedPlanId, operator, onSelectPlan, isSelected, onLoginRequested, canSelectPlan }) => {
const PlanCardWrapper: React.FC<PlanCardProps> = ({ plan, userId, subscribedPlanId, operator, onSelectPlan, isSelected, onLoginRequested, canSelectPlan }) => {
return (
<Loader>
<PlanCardComponent plan={plan} userId={userId} subscribedPlanId={subscribedPlanId} operator={operator} isSelected={isSelected} onSelectPlan={onSelectPlan} onLoginRequested={onLoginRequested} canSelectPlan={canSelectPlan}/>
<PlanCard plan={plan} userId={userId} subscribedPlanId={subscribedPlanId} operator={operator} isSelected={isSelected} onSelectPlan={onSelectPlan} onLoginRequested={onLoginRequested} canSelectPlan={canSelectPlan}/>
</Loader>
);
};
export { PlanCardWrapper as PlanCard };

View File

@ -20,6 +20,9 @@ interface PlansFilterProps {
*/
type selectOption = { value: number, label: string };
/**
* Allows filtering on plans list
*/
export const PlansFilter: React.FC<PlansFilterProps> = ({ user, groups, onGroupSelected, onError, onDurationSelected }) => {
const { t } = useTranslation('public');

View File

@ -31,7 +31,7 @@ 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, canSelectPlan }) => {
export const PlansList: React.FC<PlansListProps> = ({ onError, onPlanSelection, onLoginRequest, operator, customer, subscribedPlanId, canSelectPlan }) => {
// all plans
const [plans, setPlans] = useState<PlansTree>(null);
// all plan-categories, ordered by weight

View File

@ -14,6 +14,7 @@ import { react2angular } from 'react2angular';
import { IApplication } from '../../models/application';
import { PrepaidPack } from '../../models/prepaid-pack';
import PrepaidPackAPI from '../../api/prepaid-pack';
import { FabAlert } from '../base/fab-alert';
declare const Application: IApplication;
@ -29,7 +30,11 @@ interface PacksSummaryProps {
refresh?: Promise<void>
}
const PacksSummaryComponent: React.FC<PacksSummaryProps> = ({ item, itemType, customer, operator, onError, onSuccess, refresh }) => {
/**
* Display a short summary of the prepaid-packs already bought by the provider customer, for the given item.
* May also allows members to buy directly some new prepaid-packs.
*/
const PacksSummary: React.FC<PacksSummaryProps> = ({ item, itemType, customer, operator, onError, onSuccess, refresh }) => {
const { t } = useTranslation('logged');
const [packs, setPacks] = useState<Array<PrepaidPack>>(null);
@ -140,9 +145,9 @@ const PacksSummaryComponent: React.FC<PacksSummaryProps> = ({ item, itemType, cu
<span className="remaining-hours">
{t('app.logged.packs_summary.remaining_HOURS', { HOURS: totalHours(), ITEM: itemType })}
{isPackOnlyForSubscription && !customer.subscribed_plan &&
<div className="alert alert-warning m-t m-b">
<FabAlert level="warning">
{t('app.logged.packs_summary.unable_to_use_pack_for_subsription_is_expired')}
</div>
</FabAlert>
}
</span>
</div>
@ -178,12 +183,14 @@ const PacksSummaryComponent: React.FC<PacksSummaryProps> = ({ item, itemType, cu
);
};
export const PacksSummary: React.FC<PacksSummaryProps> = ({ item, itemType, customer, operator, onError, onSuccess, refresh }) => {
const PacksSummaryWrapper: React.FC<PacksSummaryProps> = (props) => {
return (
<Loader>
<PacksSummaryComponent item={item} itemType={itemType} customer={customer} operator={operator} onError={onError} onSuccess={onSuccess} refresh={refresh} />
<PacksSummary {...props} />
</Loader>
);
};
Application.Components.component('packsSummary', react2angular(PacksSummary, ['item', 'itemType', 'customer', 'operator', 'onError', 'onSuccess', 'refresh']));
export { PacksSummaryWrapper as PacksSummary };
Application.Components.component('packsSummary', react2angular(PacksSummaryWrapper, ['item', 'itemType', 'customer', 'operator', 'onError', 'onSuccess', 'refresh']));

View File

@ -30,6 +30,13 @@ export const EditablePrice: React.FC<EditablePriceProps> = ({ price, onSave }) =
toggleEdit();
};
/**
* Callback triggered when the user input a new price
*/
const handleChangePrice = (value: string): void => {
setTempPrice(value);
};
/**
* Enable or disable the edit mode
*/
@ -41,7 +48,7 @@ export const EditablePrice: React.FC<EditablePriceProps> = ({ price, onSave }) =
<span className="editable-price">
{!edit && <span className="display-price" onClick={toggleEdit}>{FormatLib.price(price.amount)}</span>}
{edit && <span>
<FabInput id="price" type="number" step={0.01} defaultValue={price.amount} addOn={Fablab.intl_currency} onChange={setTempPrice} required/>
<FabInput id="price" type="number" step={0.01} defaultValue={price.amount} addOn={Fablab.intl_currency} onChange={handleChangePrice} required/>
<FabButton icon={<i className="fas fa-check" />} className="approve-button" onClick={handleValidateEdit} />
<FabButton icon={<i className="fas fa-times" />} className="cancel-button" onClick={toggleEdit} />
</span>}

View File

@ -16,7 +16,7 @@ interface DeletePackProps {
* This component shows a button.
* When clicked, we show a modal dialog to ask the user for confirmation about the deletion of the provided pack.
*/
const DeletePackComponent: React.FC<DeletePackProps> = ({ onSuccess, onError, pack }) => {
const DeletePack: React.FC<DeletePackProps> = ({ onSuccess, onError, pack }) => {
const { t } = useTranslation('admin');
const [deletionModal, setDeletionModal] = useState<boolean>(false);
@ -56,10 +56,12 @@ const DeletePackComponent: React.FC<DeletePackProps> = ({ onSuccess, onError, pa
);
};
export const DeletePack: React.FC<DeletePackProps> = ({ onSuccess, onError, pack }) => {
const DeletePackWrapper: React.FC<DeletePackProps> = (props) => {
return (
<Loader>
<DeletePackComponent onSuccess={onSuccess} onError={onError} pack={pack} />
<DeletePack {...props} />
</Loader>
);
};
export { DeletePackWrapper as DeletePack };

View File

@ -28,7 +28,7 @@ interface MachinesPricingProps {
/**
* Interface to set and edit the prices of machines-hours, per group
*/
const MachinesPricing: React.FC<MachinesPricingProps> = ({ onError, onSuccess }) => {
export const MachinesPricing: React.FC<MachinesPricingProps> = ({ onError, onSuccess }) => {
const { t } = useTranslation('admin');
const [machines, setMachines] = useState<Array<Machine>>(null);

View File

@ -8,7 +8,7 @@ import FormatLib from '../../../lib/format';
import { EditExtendedPrice } from './edit-extended-price';
import { DeleteExtendedPrice } from './delete-extended-price';
interface ConfigureExtendedPriceButtonProps {
interface ConfigureExtendedPricesButtonProps {
prices: Array<Price>,
onError: (message: string) => void,
onSuccess: (message: string) => void,
@ -21,7 +21,7 @@ interface ConfigureExtendedPriceButtonProps {
* This component is a button that shows the list of extendedPrices.
* It also triggers modal dialogs to configure (add/edit/remove) extendedPrices.
*/
export const ConfigureExtendedPriceButton: React.FC<ConfigureExtendedPriceButtonProps> = ({ prices, onError, onSuccess, groupId, priceableId, priceableType }) => {
export const ConfigureExtendedPricesButton: React.FC<ConfigureExtendedPricesButtonProps> = ({ prices, onError, onSuccess, groupId, priceableId, priceableType }) => {
const { t } = useTranslation('admin');
const [extendedPrices, setExtendedPrices] = useState<Array<Price>>(prices);

View File

@ -14,7 +14,7 @@ interface ExtendedPriceFormProps {
}
/**
* A form component to create/edit a extended price.
* A form component to create/edit an extended price.
* The form validation must be created elsewhere, using the attribute form={formId}.
*/
export const ExtendedPriceForm: React.FC<ExtendedPriceFormProps> = ({ formId, onSubmit, price }) => {

View File

@ -10,7 +10,7 @@ import { Group } from '../../../models/group';
import { IApplication } from '../../../models/application';
import { Space } from '../../../models/space';
import { EditablePrice } from '../editable-price';
import { ConfigureExtendedPriceButton } from './configure-extended-price-button';
import { ConfigureExtendedPricesButton } from './configure-extended-prices-button';
import PriceAPI from '../../../api/price';
import { Price } from '../../../models/price';
import { useImmer } from 'use-immer';
@ -26,7 +26,7 @@ interface SpacesPricingProps {
/**
* Interface to set and edit the prices of spaces-hours, per group
*/
const SpacesPricing: React.FC<SpacesPricingProps> = ({ onError, onSuccess }) => {
export const SpacesPricing: React.FC<SpacesPricingProps> = ({ onError, onSuccess }) => {
const { t } = useTranslation('admin');
const [spaces, setSpaces] = useState<Array<Space>>(null);
@ -120,7 +120,7 @@ const SpacesPricing: React.FC<SpacesPricingProps> = ({ onError, onSuccess }) =>
<td>{space.name}</td>
{groups?.map(group => <td key={group.id}>
{prices.length && <EditablePrice price={findPriceBy(space.id, group.id)} onSave={handleUpdatePrice} />}
<ConfigureExtendedPriceButton
<ConfigureExtendedPricesButton
prices={findExtendedPricesBy(space.id, group.id)}
onError={onError}
onSuccess={onSuccess}

View File

@ -20,6 +20,13 @@ interface ProfileFormOptionProps {
onSuccess: (user: User) => void,
}
/**
* After first logged-in from an SSO, the user has two options:
* - complete his profile (*) ;
* - bind his profile to his existing account ;
* (*) This component handle the first case.
* It also deals with duplicate email addresses in database
*/
export const ProfileFormOption: React.FC<ProfileFormOptionProps> = ({ user, activeProvider, onError, onSuccess }) => {
const { t } = useTranslation('logged');

View File

@ -7,6 +7,7 @@ import { Loader } from '../base/loader';
import { IApplication } from '../../models/application';
import { ProfileCustomField } from '../../models/profile-custom-field';
import ProfileCustomFieldAPI from '../../api/profile-custom-field';
import { FabButton } from '../base/fab-button';
declare const Application: IApplication;
@ -18,7 +19,7 @@ interface ProfileCustomFieldsListProps {
/**
* This component shows a list of all profile custom fields
*/
const ProfileCustomFieldsList: React.FC<ProfileCustomFieldsListProps> = ({ onSuccess, onError }) => {
export const ProfileCustomFieldsList: React.FC<ProfileCustomFieldsListProps> = ({ onSuccess, onError }) => {
const { t } = useTranslation('admin');
const [profileCustomFields, setProfileCustomFields] = useState<Array<ProfileCustomField>>([]);
@ -31,6 +32,9 @@ const ProfileCustomFieldsList: React.FC<ProfileCustomFieldsListProps> = ({ onSuc
});
}, []);
/**
* Save the new state of the given custom field to the API
*/
const saveProfileCustomField = (profileCustomField: ProfileCustomField) => {
ProfileCustomFieldAPI.update(profileCustomField).then(data => {
const newFields = profileCustomFields.map(f => {
@ -43,9 +47,9 @@ const ProfileCustomFieldsList: React.FC<ProfileCustomFieldsListProps> = ({ onSuc
if (profileCustomFieldToEdit) {
setProfileCustomFieldToEdit(null);
}
onSuccess(t('app.admin.settings.compte.organization_profile_custom_field_successfully_updated'));
onSuccess(t('app.admin.settings.account.profile_custom_fields_list.field_successfully_updated'));
}).catch(err => {
onError(t('app.admin.settings.compte.organization_profile_custom_field_unable_to_update') + err);
onError(t('app.admin.settings.account.profile_custom_fields_list.unable_to_update') + err);
});
};
@ -63,12 +67,19 @@ const ProfileCustomFieldsList: React.FC<ProfileCustomFieldsListProps> = ({ onSuc
};
};
/**
* Callback triggered when the user clicks on the 'edit field' button.
* Opens the edition form for the given custom field
*/
const editProfileCustomFieldLabel = (profileCustomField: ProfileCustomField) => {
return () => {
setProfileCustomFieldToEdit(_.clone(profileCustomField));
};
};
/**
* Callback triggered when the input "label" is changed: updates the according state
*/
const onChangeProfileCustomFieldLabel = (e: BaseSyntheticEvent) => {
const { value } = e.target;
setProfileCustomFieldToEdit({
@ -77,16 +88,22 @@ const ProfileCustomFieldsList: React.FC<ProfileCustomFieldsListProps> = ({ onSuc
});
};
/**
* Save the currently edited custom field
*/
const saveProfileCustomFieldLabel = () => {
saveProfileCustomField(profileCustomFieldToEdit);
};
/**
* Closes the edition form for the currently edited custom field
*/
const cancelEditProfileCustomFieldLabel = () => {
setProfileCustomFieldToEdit(null);
};
return (
<table className="table profile-custom-fields-list">
<table className="profile-custom-fields-list">
<thead>
<tr>
<th style={{ width: '50%' }}></th>
@ -101,31 +118,44 @@ const ProfileCustomFieldsList: React.FC<ProfileCustomFieldsListProps> = ({ onSuc
<td>
{profileCustomFieldToEdit?.id !== field.id && field.label}
{profileCustomFieldToEdit?.id !== field.id && (
<button className="btn btn-default edit-profile-custom-field-label m-r-xs pull-right" onClick={editProfileCustomFieldLabel(field)}>
<FabButton className="edit-field-button" onClick={editProfileCustomFieldLabel(field)}>
<i className="fa fa-edit"></i>
</button>
</FabButton>
)}
{profileCustomFieldToEdit?.id === field.id && (
<div>
<input className="profile-custom-field-label-input" style={{ width: '80%', height: '38px' }} type="text" value={profileCustomFieldToEdit.label} onChange={onChangeProfileCustomFieldLabel} />
<span className="buttons pull-right">
<button className="btn btn-success save-profile-custom-field-label m-r-xs" onClick={saveProfileCustomFieldLabel}>
<input className="edit-field-label-input"
type="text" value={profileCustomFieldToEdit.label}
onChange={onChangeProfileCustomFieldLabel} />
<span className="buttons">
<FabButton className="save-field-label" onClick={saveProfileCustomFieldLabel}>
<i className="fa fa-check"></i>
</button>
<button className="btn btn-default delete-profile-custom-field-label m-r-xs" onClick={cancelEditProfileCustomFieldLabel}>
</FabButton>
<FabButton className="cancel-field-edition" onClick={cancelEditProfileCustomFieldLabel}>
<i className="fa fa-ban"></i>
</button>
</FabButton>
</span>
</div>
)}
</td>
<td>
<label htmlFor="profile-custom-field-actived" className="control-label m-r">{t('app.admin.settings.compte.organization_profile_custom_field.actived')}</label>
<Switch checked={field.actived} id="profile-custom-field-actived" onChange={handleSwitchChanged(field, 'actived')} className="v-middle"></Switch>
<td className="activated">
<label htmlFor="profile-custom-field-actived">
{t('app.admin.settings.account.profile_custom_fields_list.actived')}
</label>
<Switch checked={field.actived}
id="profile-custom-field-actived"
onChange={handleSwitchChanged(field, 'actived')}
className="switch"></Switch>
</td>
<td>
<label htmlFor="profile-custom-field-required" className="control-label m-r">{t('app.admin.settings.compte.organization_profile_custom_field.required')}</label>
<Switch checked={field.required} disabled={!field.actived} id="profile-custom-field-required" onChange={handleSwitchChanged(field, 'required')} className="v-middle"></Switch>
<td className="required">
<label htmlFor="profile-custom-field-required">
{t('app.admin.settings.account.profile_custom_fields_list.required')}
</label>
<Switch checked={field.required}
disabled={!field.actived}
id="profile-custom-field-required"
onChange={handleSwitchChanged(field, 'required')}
className="switch"></Switch>
</td>
</tr>
);

View File

@ -1,37 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { FabModal } from '../base/fab-modal';
import ProofOfIdentityTypeAPI from '../../api/proof-of-identity-type';
interface DeleteProofOfIdentityTypeModalProps {
isOpen: boolean,
proofOfIdentityTypeId: number,
toggleModal: () => void,
onSuccess: (message: string) => void,
onError: (message: string) => void,
}
export const DeleteProofOfIdentityTypeModal: React.FC<DeleteProofOfIdentityTypeModalProps> = ({ isOpen, toggleModal, onSuccess, proofOfIdentityTypeId, onError }) => {
const { t } = useTranslation('admin');
const handleDeleteProofOfIdentityType = async (): Promise<void> => {
try {
await ProofOfIdentityTypeAPI.destroy(proofOfIdentityTypeId);
onSuccess(t('app.admin.settings.compte.proof_of_identity_type_deleted'));
} catch (e) {
onError(t('app.admin.settings.compte.proof_of_identity_type_unable_to_delete') + e);
}
};
return (
<FabModal title={t('app.admin.settings.compte.confirmation_required')}
isOpen={isOpen}
toggleModal={toggleModal}
closeButton={true}
confirmButton={t('app.admin.settings.compte.confirm')}
onConfirm={handleDeleteProofOfIdentityType}
className="proof-of-identity-type-modal">
<p>{t('app.admin.settings.compte.do_you_really_want_to_delete_this_proof_of_identity_type')}</p>
</FabModal>
);
};

View File

@ -1,173 +0,0 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { react2angular } from 'react2angular';
import _ from 'lodash';
import { HtmlTranslate } from '../base/html-translate';
import { Loader } from '../base/loader';
import { User } from '../../models/user';
import { IApplication } from '../../models/application';
import { ProofOfIdentityType } from '../../models/proof-of-identity-type';
import { ProofOfIdentityFile } from '../../models/proof-of-identity-file';
import ProofOfIdentityTypeAPI from '../../api/proof-of-identity-type';
import ProofOfIdentityFileAPI from '../../api/proof-of-identity-file';
import { IFablab } from '../../models/fablab';
declare let Fablab: IFablab;
declare const Application: IApplication;
interface ProofOfIdentityFilesProps {
currentUser: User,
onSuccess: (message: string) => void,
onError: (message: string) => void,
}
interface FilesType {
number?: File
}
/**
* This component upload the proof of identity file of member
*/
const ProofOfIdentityFiles: React.FC<ProofOfIdentityFilesProps> = ({ currentUser, onSuccess, onError }) => {
const { t } = useTranslation('admin');
const maxProofOfIdentityFileSizeMb = (Fablab.maxProofOfIdentityFileSize / 1024 / 1024).toFixed();
// list of proof of identity type
const [proofOfIdentityTypes, setProofOfIdentityTypes] = useState<Array<ProofOfIdentityType>>([]);
const [proofOfIdentityFiles, setProofOfIdentityFiles] = useState<Array<ProofOfIdentityFile>>([]);
const [files, setFiles] = useState<FilesType>({});
const [errors, setErrors] = useState<Array<number>>([]);
// get proof of identity type and files
useEffect(() => {
ProofOfIdentityTypeAPI.index({ group_id: currentUser.group_id }).then(tData => {
setProofOfIdentityTypes(tData);
});
ProofOfIdentityFileAPI.index({ user_id: currentUser.id }).then(fData => {
setProofOfIdentityFiles(fData);
});
}, []);
const getProofOfIdentityFileByType = (proofOfIdentityTypeId: number): ProofOfIdentityFile => {
return _.find<ProofOfIdentityFile>(proofOfIdentityFiles, { proof_of_identity_type_id: proofOfIdentityTypeId });
};
const hasFile = (proofOfIdentityTypeId: number): boolean => {
return files[proofOfIdentityTypeId] || getProofOfIdentityFileByType(proofOfIdentityTypeId);
};
/**
* Check if the current collection of proof of identity types is empty or not.
*/
const hasProofOfIdentityTypes = (): boolean => {
return proofOfIdentityTypes.length > 0;
};
const onFileChange = (poitId: number) => {
return (event) => {
const fileSize = event.target.files[0].size;
let _errors = errors;
if (fileSize > Fablab.maxProofOfIdentityFileSize) {
_errors = errors.concat(poitId);
setErrors(_errors);
} else {
_errors = errors.filter(e => e !== poitId);
}
setErrors(_errors);
setFiles({
...files,
[poitId]: event.target.files[0]
});
};
};
const onFileUpload = async () => {
try {
for (const proofOfIdentityTypeId of Object.keys(files)) {
const formData = new FormData();
formData.append('proof_of_identity_file[user_id]', currentUser.id.toString());
formData.append('proof_of_identity_file[proof_of_identity_type_id]', proofOfIdentityTypeId);
formData.append('proof_of_identity_file[attachment]', files[proofOfIdentityTypeId]);
const proofOfIdentityFile = getProofOfIdentityFileByType(parseInt(proofOfIdentityTypeId, 10));
if (proofOfIdentityFile) {
await ProofOfIdentityFileAPI.update(proofOfIdentityFile.id, formData);
} else {
await ProofOfIdentityFileAPI.create(formData);
}
}
if (Object.keys(files).length > 0) {
ProofOfIdentityFileAPI.index({ user_id: currentUser.id }).then(fData => {
setProofOfIdentityFiles(fData);
setFiles({});
onSuccess(t('app.admin.members_edit.proof_of_identity_files_successfully_uploaded'));
});
}
} catch (e) {
onError(t('app.admin.members_edit.proof_of_identity_files_unable_to_upload') + e);
}
};
const getProofOfIdentityFileUrl = (poifId: number) => {
return `/api/proof_of_identity_files/${poifId}/download`;
};
return (
<section className="panel panel-default bg-light m-lg col-sm-12 col-md-12 col-lg-9">
<h3>{t('app.admin.members_edit.proof_of_identity_files')}</h3>
<p className="text-black font-sbold">{t('app.admin.members_edit.my_documents_info')}</p>
<div className="alert alert-warning">
<HtmlTranslate trKey="app.admin.members_edit.my_documents_alert" options={{ SIZE: maxProofOfIdentityFileSizeMb }}/>
</div>
<div className="widget-content no-bg auto">
{proofOfIdentityTypes.map((poit: ProofOfIdentityType) => {
return (
<div className={`form-group ${errors.includes(poit.id) ? 'has-error' : ''}`} key={poit.id}>
<label className="control-label m-r">{poit.name}</label>
<div className="fileinput input-group">
<div className="form-control">
{hasFile(poit.id) && (
<div>
<i className="glyphicon glyphicon-file fileinput-exists"></i> <span className="fileinput-filename">{files[poit.id]?.name || getProofOfIdentityFileByType(poit.id).attachment}</span>
</div>
)}
{getProofOfIdentityFileByType(poit.id) && !files[poit.id] && (
<a href={getProofOfIdentityFileUrl(getProofOfIdentityFileByType(poit.id).id)} target="_blank" style={{ position: 'absolute', right: '10px' }} rel="noreferrer"><i className="fa fa-download text-black "></i></a>
)}
</div>
<span className="input-group-addon btn btn-default btn-file">
{!hasFile(poit.id) && (
<span className="fileinput-new">Parcourir</span>
)}
{hasFile(poit.id) && (
<span className="fileinput-exists">Modifier</span>
)}
<input type="file"
accept="application/pdf,image/jpeg,image/jpg,image/png"
onChange={onFileChange(poit.id)}
required />
</span>
</div>
{errors.includes(poit.id) && <span className="help-block">{t('app.admin.members_edit.proof_of_identity_file_size_error', { SIZE: maxProofOfIdentityFileSizeMb })}</span>}
</div>
);
})}
</div>
{hasProofOfIdentityTypes() && (
<button type="button" className="btn btn-warning m-b m-t pull-right" onClick={onFileUpload} disabled={errors.length > 0}>{t('app.admin.members_edit.save')}</button>
)}
</section>
);
};
const ProofOfIdentityFilesWrapper: React.FC<ProofOfIdentityFilesProps> = ({ currentUser, onSuccess, onError }) => {
return (
<Loader>
<ProofOfIdentityFiles currentUser={currentUser} onSuccess={onSuccess} onError={onError} />
</Loader>
);
};
Application.Components.component('proofOfIdentityFiles', react2angular(ProofOfIdentityFilesWrapper, ['currentUser', 'onSuccess', 'onError']));

View File

@ -1,63 +0,0 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FabModal } from '../base/fab-modal';
import { ProofOfIdentityType } from '../../models/proof-of-identity-type';
import { ProofOfIdentityRefusal } from '../../models/proof-of-identity-refusal';
import { User } from '../../models/user';
import ProofOfIdentityRefusalAPI from '../../api/proof-of-identity-refusal';
import { ProofOfIdentityRefusalForm } from './proof-of-identity-refusal-form';
interface ProofOfIdentityRefusalModalProps {
isOpen: boolean,
toggleModal: () => void,
onSuccess: (message: string) => void,
onError: (message: string) => void,
proofOfIdentityTypes: Array<ProofOfIdentityType>,
operator: User,
member: User
}
export const ProofOfIdentityRefusalModal: React.FC<ProofOfIdentityRefusalModalProps> = ({ isOpen, toggleModal, onSuccess, proofOfIdentityTypes, operator, member, onError }) => {
const { t } = useTranslation('admin');
const [data, setData] = useState<ProofOfIdentityRefusal>({
id: null,
operator_id: operator.id,
user_id: member.id,
proof_of_identity_type_ids: [],
message: ''
});
const handleProofOfIdentityRefusalChanged = (field: string, value: string | Array<number>) => {
setData({
...data,
[field]: value
});
};
const handleSaveProofOfIdentityRefusal = async (): Promise<void> => {
try {
await ProofOfIdentityRefusalAPI.create(data);
onSuccess(t('app.admin.members_edit.proof_of_identity_refusal_successfully_sent'));
} catch (e) {
onError(t('app.admin.members_edit.proof_of_identity_refusal_unable_to_send') + e);
}
};
const isPreventSaveProofOfIdentityRefusal = (): boolean => {
return !data.message || data.proof_of_identity_type_ids.length === 0;
};
return (
<FabModal title={t('app.admin.members_edit.proof_of_identity_refusal')}
isOpen={isOpen}
toggleModal={toggleModal}
closeButton={false}
confirmButton={t('app.admin.members_edit.confirm')}
onConfirm={handleSaveProofOfIdentityRefusal}
preventConfirm={isPreventSaveProofOfIdentityRefusal()}
className="proof-of-identity-type-modal">
<ProofOfIdentityRefusalForm proofOfIdentityTypes={proofOfIdentityTypes} onChange={handleProofOfIdentityRefusalChanged}/>
</FabModal>
);
};

View File

@ -1,68 +0,0 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { FabModal } from '../base/fab-modal';
import { ProofOfIdentityType } from '../../models/proof-of-identity-type';
import { Group } from '../../models/group';
import ProofOfIdentityTypeAPI from '../../api/proof-of-identity-type';
import { ProofOfIdentityTypeForm } from './proof-of-identity-type-form';
interface ProofOfIdentityTypeModalProps {
isOpen: boolean,
toggleModal: () => void,
onSuccess: (message: string) => void,
onError: (message: string) => void,
groups: Array<Group>,
proofOfIdentityType?: ProofOfIdentityType,
}
export const ProofOfIdentityTypeModal: React.FC<ProofOfIdentityTypeModalProps> = ({ isOpen, toggleModal, onSuccess, onError, proofOfIdentityType, groups }) => {
const { t } = useTranslation('admin');
const [data, setData] = useState<ProofOfIdentityType>({ id: proofOfIdentityType?.id, group_ids: proofOfIdentityType?.group_ids || [], name: proofOfIdentityType?.name || '' });
useEffect(() => {
setData({ id: proofOfIdentityType?.id, group_ids: proofOfIdentityType?.group_ids || [], name: proofOfIdentityType?.name || '' });
}, [proofOfIdentityType]);
const handleProofOfIdentityTypeChanged = (field: string, value: string | Array<number>) => {
setData({
...data,
[field]: value
});
};
const handleSaveProofOfIdentityType = async (): Promise<void> => {
try {
if (proofOfIdentityType?.id) {
await ProofOfIdentityTypeAPI.update(data);
onSuccess(t('app.admin.settings.compte.proof_of_identity_type_successfully_updated'));
} else {
await ProofOfIdentityTypeAPI.create(data);
onSuccess(t('app.admin.settings.compte.proof_of_identity_type_successfully_created'));
}
} catch (e) {
if (proofOfIdentityType?.id) {
onError(t('app.admin.settings.compte.proof_of_identity_type_unable_to_update') + e);
} else {
onError(t('app.admin.settings.compte.proof_of_identity_type_unable_to_create') + e);
}
}
};
const isPreventSaveProofOfIdentityType = (): boolean => {
return !data.name || data.group_ids.length === 0;
};
return (
<FabModal title={t(`app.admin.settings.compte.${proofOfIdentityType ? 'edit' : 'new'}_proof_of_identity_type`)}
isOpen={isOpen}
toggleModal={toggleModal}
closeButton={false}
confirmButton={t(`app.admin.settings.compte.${proofOfIdentityType ? 'edit' : 'create'}`)}
onConfirm={handleSaveProofOfIdentityType}
preventConfirm={isPreventSaveProofOfIdentityType()}
className="proof-of-identity-type-modal">
<ProofOfIdentityTypeForm proofOfIdentityType={proofOfIdentityType} groups={groups} onChange={handleProofOfIdentityTypeChanged}/>
</FabModal>
);
};

View File

@ -1,214 +0,0 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { react2angular } from 'react2angular';
import _ from 'lodash';
import { HtmlTranslate } from '../base/html-translate';
import { Loader } from '../base/loader';
import { IApplication } from '../../models/application';
import { ProofOfIdentityType } from '../../models/proof-of-identity-type';
import { Group } from '../../models/group';
import { ProofOfIdentityTypeModal } from './proof-of-identity-type-modal';
import { DeleteProofOfIdentityTypeModal } from './delete-proof-of-identity-type-modal';
import GroupAPI from '../../api/group';
import ProofOfIdentityTypeAPI from '../../api/proof-of-identity-type';
declare const Application: IApplication;
interface ProofOfIdentityTypesListProps {
onSuccess: (message: string) => void,
onError: (message: string) => void,
}
/**
* This component shows a list of all payment schedules with their associated deadlines (aka. PaymentScheduleItem) and invoices
*/
const ProofOfIdentityTypesList: React.FC<ProofOfIdentityTypesListProps> = ({ onSuccess, onError }) => {
const { t } = useTranslation('admin');
// list of displayed proof of identity type
const [proofOfIdentityTypes, setProofOfIdentityTypes] = useState<Array<ProofOfIdentityType>>([]);
const [proofOfIdentityType, setProofOfIdentityType] = useState<ProofOfIdentityType>(null);
const [proofOfIdentityTypeOrder, setProofOfIdentityTypeOrder] = useState<string>(null);
const [modalIsOpen, setModalIsOpen] = useState<boolean>(false);
const [groups, setGroups] = useState<Array<Group>>([]);
const [destroyModalIsOpen, setDestroyModalIsOpen] = useState<boolean>(false);
const [proofOfIdentityTypeId, setProofOfIdentityTypeId] = useState<number>(null);
// get groups
useEffect(() => {
GroupAPI.index({ disabled: false, admins: false }).then(data => {
setGroups(data);
ProofOfIdentityTypeAPI.index().then(pData => {
setProofOfIdentityTypes(pData);
});
});
}, []);
/**
* Check if the current collection of proof of identity types is empty or not.
*/
const hasProofOfIdentityTypes = (): boolean => {
return proofOfIdentityTypes.length > 0;
};
const addProofOfIdentityType = (): void => {
setProofOfIdentityType(null);
setModalIsOpen(true);
};
const editProofOfIdentityType = (poit: ProofOfIdentityType): () => void => {
return (): void => {
setProofOfIdentityType(poit);
setModalIsOpen(true);
};
};
const toggleCreateAndEditModal = (): void => {
setModalIsOpen(false);
};
const saveProofOfIdentityTypeOnSuccess = (message: string): void => {
setModalIsOpen(false);
ProofOfIdentityTypeAPI.index().then(pData => {
setProofOfIdentityTypes(orderProofOfIdentityTypes(pData, proofOfIdentityTypeOrder));
onSuccess(message);
}).catch((error) => {
onError('Unable to load proof of identity types' + error);
});
};
const destroyProofOfIdentityType = (id: number): () => void => {
return (): void => {
setProofOfIdentityTypeId(id);
setDestroyModalIsOpen(true);
};
};
const toggleDestroyModal = (): void => {
setDestroyModalIsOpen(false);
};
const destroyProofOfIdentityTypeOnSuccess = (message: string): void => {
setDestroyModalIsOpen(false);
ProofOfIdentityTypeAPI.index().then(pData => {
setProofOfIdentityTypes(pData);
setProofOfIdentityTypes(orderProofOfIdentityTypes(pData, proofOfIdentityTypeOrder));
onSuccess(message);
}).catch((error) => {
onError('Unable to load proof of identity types' + error);
});
};
const setOrderProofOfIdentityType = (orderBy: string): () => void => {
return () => {
let order = orderBy;
if (proofOfIdentityTypeOrder === orderBy) {
order = `-${orderBy}`;
}
setProofOfIdentityTypeOrder(order);
setProofOfIdentityTypes(orderProofOfIdentityTypes(proofOfIdentityTypes, order));
};
};
const orderProofOfIdentityTypes = (poits: Array<ProofOfIdentityType>, orderBy?: string): Array<ProofOfIdentityType> => {
if (!orderBy) {
return poits;
}
const order = orderBy[0] === '-' ? 'desc' : 'asc';
if (orderBy.search('group_name') !== -1) {
return _.orderBy(poits, (poit: ProofOfIdentityType) => getGroupName(poit.group_ids), order);
} else {
return _.orderBy(poits, 'name', order);
}
};
const orderClassName = (orderBy: string): string => {
if (proofOfIdentityTypeOrder) {
const order = proofOfIdentityTypeOrder[0] === '-' ? proofOfIdentityTypeOrder.substr(1) : proofOfIdentityTypeOrder;
if (order === orderBy) {
return `fa fa-arrows-v ${proofOfIdentityTypeOrder[0] === '-' ? 'fa-sort-alpha-desc' : 'fa-sort-alpha-asc'}`;
}
}
return 'fa fa-arrows-v';
};
const getGroupName = (groupIds: Array<number>): string => {
if (groupIds.length === groups.length && groupIds.length > 0) {
return t('app.admin.settings.compte.all_groups');
}
const _groups = _.filter(groups, (g: Group) => { return groupIds.includes(g.id); });
return _groups.map((g: Group) => g.name).join(', ');
};
return (
<div className="panel panel-default m-t-md">
<div className="panel-heading">
<span className="font-sbold">{t('app.admin.settings.compte.add_proof_of_identity_types')}</span>
</div>
<div className="panel-body">
<div className="row">
<p className="m-h">{t('app.admin.settings.compte.proof_of_identity_type_info')}</p>
<div className="alert alert-warning m-h-md row">
<div className="col-md-8">
<HtmlTranslate trKey="app.admin.settings.compte.proof_of_identity_type_no_group_info" />
</div>
<a href="/#!/admin/members?tabs=1" className="btn btn-warning pull-right m-t m-r-md col-md-3" style={{ color: '#000', maxWidth: '200px' }}>{t('app.admin.settings.compte.create_groups')}</a>
</div>
</div>
<div className="row">
<h3 className="m-l inline">{t('app.admin.settings.compte.proof_of_identity_type_title')}</h3>
<button name="button" className="btn btn-warning pull-right m-t m-r-md" onClick={addProofOfIdentityType}>{t('app.admin.settings.compte.add_proof_of_identity_type_button')}</button>
</div>
<ProofOfIdentityTypeModal isOpen={modalIsOpen} groups={groups} proofOfIdentityType={proofOfIdentityType} toggleModal={toggleCreateAndEditModal} onSuccess={saveProofOfIdentityTypeOnSuccess} onError={onError} />
<DeleteProofOfIdentityTypeModal isOpen={destroyModalIsOpen} proofOfIdentityTypeId={proofOfIdentityTypeId} toggleModal={toggleDestroyModal} onSuccess={destroyProofOfIdentityTypeOnSuccess} onError={onError}/>
<table className="table proof-of-identity-type-list">
<thead>
<tr>
<th style={{ width: '40%' }}><a onClick={setOrderProofOfIdentityType('group_name')}>{t('app.admin.settings.compte.proof_of_identity_type.group_name')} <i className={orderClassName('group_name')}></i></a></th>
<th style={{ width: '40%' }}><a onClick={setOrderProofOfIdentityType('name')}>{t('app.admin.settings.compte.proof_of_identity_type.name')} <i className={orderClassName('name')}></i></a></th>
<th style={{ width: '20%' }} className="buttons-col"></th>
</tr>
</thead>
<tbody>
{proofOfIdentityTypes.map(poit => {
return (
<tr key={poit.id}>
<td>{getGroupName(poit.group_ids)}</td>
<td>{poit.name}</td>
<td>
<div className="buttons">
<button className="btn btn-default edit-proof-of-identity-type m-r-xs" onClick={editProofOfIdentityType(poit)}>
<i className="fa fa-edit"></i>
</button>
<button className="btn btn-danger delete-proof-of-identity-type" onClick={destroyProofOfIdentityType(poit.id)}>
<i className="fa fa-trash"></i>
</button>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
{!hasProofOfIdentityTypes() && (
<p className="text-center">
<HtmlTranslate trKey="app.admin.settings.compte.no_proof_of_identity_types" />
</p>
)}
</div>
</div>
);
};
const ProofOfIdentityTypesListWrapper: React.FC<ProofOfIdentityTypesListProps> = ({ onSuccess, onError }) => {
return (
<Loader>
<ProofOfIdentityTypesList onSuccess={onSuccess} onError={onError} />
</Loader>
);
};
Application.Components.component('proofOfIdentityTypesList', react2angular(ProofOfIdentityTypesListWrapper, ['onSuccess', 'onError']));

View File

@ -1,121 +0,0 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { react2angular } from 'react2angular';
import _ from 'lodash';
import { Loader } from '../base/loader';
import { User } from '../../models/user';
import { IApplication } from '../../models/application';
import { ProofOfIdentityType } from '../../models/proof-of-identity-type';
import { ProofOfIdentityFile } from '../../models/proof-of-identity-file';
import ProofOfIdentityTypeAPI from '../../api/proof-of-identity-type';
import ProofOfIdentityFileAPI from '../../api/proof-of-identity-file';
import { ProofOfIdentityRefusalModal } from './proof-of-identity-refusal-modal';
declare const Application: IApplication;
interface ProofOfIdentityValidationProps {
operator: User,
member: User
onSuccess: (message: string) => void,
onError: (message: string) => void,
}
/**
* This component shows a list of proof of identity file of member, admin can download and valid
**/
const ProofOfIdentityValidation: React.FC<ProofOfIdentityValidationProps> = ({ operator, member, onSuccess, onError }) => {
const { t } = useTranslation('admin');
// list of proof of identity type
const [proofOfIdentityTypes, setProofOfIdentityTypes] = useState<Array<ProofOfIdentityType>>([]);
const [proofOfIdentityFiles, setProofOfIdentityFiles] = useState<Array<ProofOfIdentityFile>>([]);
const [modalIsOpen, setModalIsOpen] = useState<boolean>(false);
// get groups
useEffect(() => {
ProofOfIdentityTypeAPI.index({ group_id: member.group_id }).then(tData => {
setProofOfIdentityTypes(tData);
});
ProofOfIdentityFileAPI.index({ user_id: member.id }).then(fData => {
setProofOfIdentityFiles(fData);
});
}, []);
const getProofOfIdentityFileByType = (proofOfIdentityTypeId: number): ProofOfIdentityFile => {
return _.find<ProofOfIdentityFile>(proofOfIdentityFiles, { proof_of_identity_type_id: proofOfIdentityTypeId });
};
/**
* Check if the current collection of proof of identity types is empty or not.
*/
const hasProofOfIdentityTypes = (): boolean => {
return proofOfIdentityTypes.length > 0;
};
const getProofOfIdentityFileUrl = (poifId: number): string => {
return `/api/proof_of_identity_files/${poifId}/download`;
};
const openProofOfIdentityRefusalModal = (): void => {
setModalIsOpen(true);
};
const toggleModal = (): void => {
setModalIsOpen(false);
};
const saveProofOfIdentityRefusalOnSuccess = (message: string): void => {
setModalIsOpen(false);
onSuccess(message);
};
return (
<div>
<section className="panel panel-default bg-light m-lg col-sm-12 col-md-12 col-lg-7">
<h3>{t('app.admin.members_edit.proof_of_identity_files')}</h3>
<p className="text-black font-sbold">{t('app.admin.members_edit.find_below_the_proof_of_identity_files')}</p>
{proofOfIdentityTypes.map((poit: ProofOfIdentityType) => {
return (
<div key={poit.id} className="m-b">
<div className="m-b-xs">{poit.name}</div>
{getProofOfIdentityFileByType(poit.id) && (
<a href={getProofOfIdentityFileUrl(getProofOfIdentityFileByType(poit.id).id)} target="_blank" rel="noreferrer">
<span className="m-r">{getProofOfIdentityFileByType(poit.id).attachment}</span>
<i className="fa fa-download"></i>
</a>
)}
{!getProofOfIdentityFileByType(poit.id) && (
<div className="text-danger">{t('app.admin.members_edit.to_complete')}</div>
)}
</div>
);
})}
</section>
{hasProofOfIdentityTypes() && !member.validated_at && (
<section className="panel panel-default bg-light m-t-lg col-sm-12 col-md-12 col-lg-4">
<h3>{t('app.admin.members_edit.refuse_proof_of_identity_files')}</h3>
<p className="text-black">{t('app.admin.members_edit.refuse_proof_of_identity_files_info')}</p>
<button type="button" className="btn btn-warning m-b m-t" onClick={openProofOfIdentityRefusalModal}>{t('app.admin.members_edit.proof_of_identity_refusal')}</button>
<ProofOfIdentityRefusalModal
isOpen={modalIsOpen}
proofOfIdentityTypes={proofOfIdentityTypes}
toggleModal={toggleModal}
operator={operator}
member={member}
onError={onError}
onSuccess={saveProofOfIdentityRefusalOnSuccess}/>
</section>
)}
</div>
);
};
const ProofOfIdentityValidationWrapper: React.FC<ProofOfIdentityValidationProps> = ({ operator, member, onSuccess, onError }) => {
return (
<Loader>
<ProofOfIdentityValidation operator={operator} member={member} onSuccess={onSuccess} onError={onError} />
</Loader>
);
};
Application.Components.component('proofOfIdentityValidation', react2angular(ProofOfIdentityValidationWrapper, ['operator', 'member', 'onSuccess', 'onError']));

View File

@ -48,17 +48,21 @@ export const BooleanSetting: React.FC<BooleanSettingProps> = ({ name, label, cla
*/
const updateSetting = () => {
SettingAPI.update(name, value ? 'true' : 'false')
.then(() => onSuccess(t('app.admin.settings.customization_of_SETTING_successfully_saved', { SETTING: t(`app.admin.settings.${name}`) })))
.then(() => onSuccess(t('app.admin.boolean_setting.customization_of_SETTING_successfully_saved', {
SETTING: t(`app.admin.settings.${name}`) // eslint-disable-line fabmanager/scoped-translation
})))
.catch(err => {
if (err.status === 304) return;
if (err.status === 423) {
onError(t('app.admin.settings.error_SETTING_locked', { SETTING: t(`app.admin.settings.${name}`) }));
onError(t('app.admin.boolean_setting.error_SETTING_locked', {
SETTING: t(`app.admin.settings.${name}`) // eslint-disable-line fabmanager/scoped-translation
}));
return;
}
console.log(err);
onError(t('app.admin.settings.an_error_occurred_saving_the_setting'));
onError(t('app.admin.boolean_setting.an_error_occurred_saving_the_setting'));
});
};
@ -97,15 +101,15 @@ export const BooleanSetting: React.FC<BooleanSettingProps> = ({ name, label, cla
};
return (
<div className={`form-group ${className || ''}`}>
<label htmlFor={`setting-${name}`} className="control-label m-r">{label}</label>
<Switch checked={value} id={`setting-${name}}`} onChange={handleChanged} className="v-middle"></Switch>
{!hideSave && <FabButton className="btn btn-warning m-l" onClick={handleSave}>{t('app.admin.check_list_setting.save')}</FabButton> }
<div className={`boolean-setting ${className || ''}`}>
<label htmlFor={`setting-${name}`}>{label}</label>
<Switch checked={value} id={`setting-${name}}`} onChange={handleChanged} className="switch"></Switch>
{!hideSave && <FabButton className="save-btn" onClick={handleSave}>{t('app.admin.boolean_setting.save')}</FabButton> }
</div>
);
};
export const BooleanSettingWrapper: React.FC<BooleanSettingProps> = ({ onChange, onSuccess, onError, label, className, name, hideSave, onBeforeSave }) => {
const BooleanSettingWrapper: React.FC<BooleanSettingProps> = ({ onChange, onSuccess, onError, label, className, name, hideSave, onBeforeSave }) => {
return (
<Loader>
<BooleanSetting label={label} name={name} onError={onError} onSuccess={onSuccess} onChange={onChange} className={className} hideSave={hideSave} onBeforeSave={onBeforeSave} />

View File

@ -75,7 +75,9 @@ export const CheckListSetting: React.FC<CheckListSettingProps> = ({ name, label,
*/
const handleSave = () => {
SettingAPI.update(name, value)
.then(() => onSuccess(t('app.admin.check_list_setting.customization_of_SETTING_successfully_saved', { SETTING: t(`app.admin.settings.${name}`) })))
.then(() => onSuccess(t('app.admin.check_list_setting.customization_of_SETTING_successfully_saved', {
SETTING: t(`app.admin.settings.${name}`) // eslint-disable-line fabmanager/scoped-translation
})))
.catch(err => onError(err));
};
@ -98,7 +100,7 @@ export const CheckListSetting: React.FC<CheckListSettingProps> = ({ name, label,
);
};
export const CheckListSettingWrapper: React.FC<CheckListSettingProps> = ({ availableOptions, onSuccess, onError, label, className, name, hideSave, defaultValue, onChange }) => {
const CheckListSettingWrapper: React.FC<CheckListSettingProps> = ({ availableOptions, onSuccess, onError, label, className, name, hideSave, defaultValue, onChange }) => {
return (
<Loader>
<CheckListSetting availableOptions={availableOptions} label={label} name={name} onError={onError} onSuccess={onSuccess} className={className} hideSave={hideSave} defaultValue={defaultValue} onChange={onChange} />

View File

@ -8,6 +8,7 @@ import { Loader } from '../base/loader';
import { FabButton } from '../base/fab-button';
import { BooleanSetting } from './boolean-setting';
import { CheckListSetting } from './check-list-setting';
import { FabAlert } from '../base/fab-alert';
declare const Application: IApplication;
@ -17,16 +18,16 @@ interface UserValidationSettingProps {
}
/**
* This component allows to configure user validation required setting.
* This component allows an admin to configure the settings related to the user account validation.
*/
const UserValidationSetting: React.FC<UserValidationSettingProps> = ({ onSuccess, onError }) => {
export const UserValidationSetting: React.FC<UserValidationSettingProps> = ({ onSuccess, onError }) => {
const { t } = useTranslation('admin');
const [userValidationRequired, setUserValidationRequired] = useState<string>('false');
const userValidationRequiredListDefault = ['subscription', 'machine', 'event', 'space', 'training', 'pack'];
const [userValidationRequiredList, setUserValidationRequiredList] = useState<string>(null);
const userValidationRequiredOptions = userValidationRequiredListDefault.map(l => {
return [l, t(`app.admin.settings.compte.user_validation_required_list.${l}`)];
return [l, t(`app.admin.settings.account.user_validation_setting.user_validation_required_list.${l}`)];
});
/**
@ -36,20 +37,24 @@ const UserValidationSetting: React.FC<UserValidationSettingProps> = ({ onSuccess
SettingAPI.update(name, value)
.then(() => {
if (name === SettingName.UserValidationRequired) {
onSuccess(t('app.admin.settings.customization_of_SETTING_successfully_saved', { SETTING: t(`app.admin.settings.compte.${name}`) }));
onSuccess(t('app.admin.settings.account.user_validation_setting.customization_of_SETTING_successfully_saved', {
SETTING: t(`app.admin.settings.account.${name}`) // eslint-disable-line fabmanager/scoped-translation
}));
}
}).catch(err => {
if (err.status === 304) return;
if (err.status === 423) {
if (name === SettingName.UserValidationRequired) {
onError(t('app.admin.settings.error_SETTING_locked', { SETTING: t(`app.admin.settings.compte.${name}`) }));
onError(t('app.admin.settings.account.user_validation_setting.error_SETTING_locked', {
SETTING: t(`app.admin.settings.account.${name}`) // eslint-disable-line fabmanager/scoped-translation
}));
}
return;
}
console.log(err);
onError(t('app.admin.settings.an_error_occurred_saving_the_setting'));
onError(t('app.admin.settings.account.user_validation_setting.an_error_occurred_saving_the_setting'));
});
};
@ -70,7 +75,7 @@ const UserValidationSetting: React.FC<UserValidationSettingProps> = ({ onSuccess
return (
<div className="user-validation-setting">
<BooleanSetting name={SettingName.UserValidationRequired}
label={t('app.admin.settings.compte.user_validation_required_option_label')}
label={t('app.admin.settings.account.user_validation_setting.user_validation_required_option_label')}
hideSave={true}
onChange={setUserValidationRequired}
onSuccess={onSuccess}
@ -78,13 +83,13 @@ const UserValidationSetting: React.FC<UserValidationSettingProps> = ({ onSuccess
</BooleanSetting>
{userValidationRequired === 'true' &&
<div>
<h4>{t('app.admin.settings.compte.user_validation_required_list_title')}</h4>
<h4>{t('app.admin.settings.account.user_validation_setting.user_validation_required_list_title')}</h4>
<p>
{t('app.admin.settings.compte.user_validation_required_list_info')}
</p>
<p className="alert alert-warning">
{t('app.admin.settings.compte.user_validation_required_list_other_info')}
{t('app.admin.settings.account.user_validation_setting.user_validation_required_list_info')}
</p>
<FabAlert level="warning">
{t('app.admin.settings.account.user_validation_setting.user_validation_required_list_other_info')}
</FabAlert>
<CheckListSetting name={SettingName.UserValidationRequiredList}
label=""
availableOptions={userValidationRequiredOptions}
@ -96,7 +101,7 @@ const UserValidationSetting: React.FC<UserValidationSettingProps> = ({ onSuccess
</CheckListSetting>
</div>
}
<FabButton className="btn btn-warning m-t" onClick={handleSave}>{t('app.admin.check_list_setting.save')}</FabButton>
<FabButton className="save-btn" onClick={handleSave}>{t('app.admin.settings.account.user_validation_setting.save')}</FabButton>
</div>
);
};

View File

@ -16,6 +16,9 @@ interface EditSocialsProps<TFieldValues> {
disabled: boolean|((id: string) => boolean),
}
/**
* Allow a user to edit its personnal social networks
*/
export const EditSocials = <TFieldValues extends FieldValues>({ register, setValue, networks, formState, disabled }: EditSocialsProps<TFieldValues>) => {
const { t } = useTranslation('shared');
// regular expression to validate the the input fields
@ -23,10 +26,17 @@ export const EditSocials = <TFieldValues extends FieldValues>({ register, setVal
const initSelectedNetworks = networks.filter(el => !['', null, undefined].includes(el.url));
const [selectedNetworks, setSelectedNetworks] = useState(initSelectedNetworks);
/**
* Callback triggered when the user adds a network, from the list of available networks, to the editable networks.
*/
const selectNetwork = (network) => {
setSelectedNetworks([...selectedNetworks, network]);
};
/**
* Return a derivated state of the selected networks list, depending on the given action.
*/
const reducer = (state, action) => {
switch (action.type) {
case 'delete':
@ -61,14 +71,14 @@ export const EditSocials = <TFieldValues extends FieldValues>({ register, setVal
rules= {{
pattern: {
value: urlRegex,
message: t('app.shared.user_profile_form.website_invalid')
message: t('app.shared.edit_socials.website_invalid')
}
}}
formState={formState}
defaultValue={network.url}
label={network.name}
disabled={disabled}
placeholder={t('app.shared.text_editor.url_placeholder')}
placeholder={t('app.shared.edit_socials.url_placeholder')}
icon={<img src={`${Icons}#${network.name}`}></img>}
addOn={<Trash size={16} />}
addOnAction={() => dispatch({ type: 'delete', payload: { network, field: `profile_attributes.${network.name}` } })} />

View File

@ -20,6 +20,9 @@ interface FabSocialsProps {
onSuccess: (message: string) => void
}
/**
* Allows the Fablab to edit its corporate social networks, or to display them read-only to the end users (show=true)
*/
export const FabSocials: React.FC<FabSocialsProps> = ({ show = false, onError, onSuccess }) => {
const { t } = useTranslation('shared');
// regular expression to validate the the input fields
@ -42,6 +45,9 @@ export const FabSocials: React.FC<FabSocialsProps> = ({ show = false, onError, o
setSelectedNetworks(fabNetworks.filter(el => el.url !== ''));
}, [fabNetworks]);
/**
* Callback triggered when the social networks are saved
*/
const onSubmit = (data) => {
const updatedNetworks = new Map<SettingName, string>();
Object.keys(data).forEach(key => updatedNetworks.set(key as SettingName, data[key]));
@ -55,17 +61,24 @@ export const FabSocials: React.FC<FabSocialsProps> = ({ show = false, onError, o
});
};
/**
* Callback triggered when the user adds a network, from the list of available networks, to the editable networks.
*/
const selectNetwork = (network) => {
setSelectedNetworks([...selectedNetworks, network]);
};
/**
* Callback triggered when the user removes a network, from the list of editables networks, add put it back to the
* list of avaiable networks.
*/
const remove = (network) => {
setSelectedNetworks(selectedNetworks.filter(el => el !== network));
setValue(network.name, '');
};
return (
<>{show
<div className="fab-socials">{show
? (selectedNetworks.length > 0) && <>
<h2>{t('app.shared.fab_socials.follow_us')}</h2>
<div className='social-icons'>
@ -94,7 +107,7 @@ export const FabSocials: React.FC<FabSocialsProps> = ({ show = false, onError, o
rules={{
pattern: {
value: urlRegex,
message: t('app.shared.user_profile_form.website_invalid')
message: t('app.shared.fab_socials.website_invalid')
}
}}
formState={formState}
@ -107,11 +120,11 @@ export const FabSocials: React.FC<FabSocialsProps> = ({ show = false, onError, o
)}
</div>}
<FabButton type='submit'
className='btn-warning'>
{t('app.shared.buttons.save')}
className='save-btn'>
{t('app.shared.fab_socials.save')}
</FabButton>
</form>
}</>
}</div>
);
};

View File

@ -26,7 +26,7 @@ interface FreeExtendModalProps {
/**
* Modal dialog shown to extend the current subscription of a customer, for free
*/
const FreeExtendModal: React.FC<FreeExtendModalProps> = ({ isOpen, toggleModal, subscription, customerId, onError, onSuccess }) => {
export const FreeExtendModal: React.FC<FreeExtendModalProps> = ({ isOpen, toggleModal, subscription, customerId, onError, onSuccess }) => {
// we do not render the modal if the subscription was not provided
if (!subscription) return null;

View File

@ -35,7 +35,7 @@ interface RenewModalProps {
/**
* Modal dialog shown to renew the current subscription of a customer, for free
*/
const RenewModal: React.FC<RenewModalProps> = ({ isOpen, toggleModal, subscription, customer, operator, onError, onSuccess }) => {
export const RenewModal: React.FC<RenewModalProps> = ({ isOpen, toggleModal, subscription, customer, operator, onError, onSuccess }) => {
// we do not render the modal if the subscription was not provided
if (!subscription) return null;
@ -85,14 +85,14 @@ const RenewModal: React.FC<RenewModalProps> = ({ isOpen, toggleModal, subscripti
* Return the formatted localized date for the given date
*/
const formatDateTime = (date: Date|TDateISO): string => {
return t('app.admin.free_extend_modal.DATE_TIME', { DATE: FormatLib.date(date), TIME: FormatLib.time(date) });
return t('app.admin.renew_modal.DATE_TIME', { DATE: FormatLib.date(date), TIME: FormatLib.time(date) });
};
/**
* Callback triggered when the payment of the subscription renewal was successful
*/
const onPaymentSuccess = (): void => {
onSuccess(t('app.admin.renew_subscription_modal.renew_success'), expirationDate);
onSuccess(t('app.admin.renew_modal.renew_success'), expirationDate);
toggleModal();
};
@ -108,25 +108,25 @@ const RenewModal: React.FC<RenewModalProps> = ({ isOpen, toggleModal, subscripti
toggleModal={toggleModal}
width={ModalSize.large}
className="renew-modal"
title={t('app.admin.renew_subscription_modal.renew_subscription')}
confirmButton={t('app.admin.renew_subscription_modal.renew')}
title={t('app.admin.renew_modal.renew_subscription')}
confirmButton={t('app.admin.renew_modal.renew')}
onConfirm={toggleLocalPaymentModal}
closeButton>
<FabAlert level="danger" className="conditions">
<p>{t('app.admin.renew_subscription_modal.renew_subscription_info')}</p>
<p>{t('app.admin.renew_subscription_modal.credits_will_be_reset')}</p>
<p>{t('app.admin.renew_modal.renew_subscription_info')}</p>
<p>{t('app.admin.renew_modal.credits_will_be_reset')}</p>
</FabAlert>
<div className="form-and-payment">
<form className="configuration-form">
<label htmlFor="current_expiration">{t('app.admin.renew_subscription_modal.current_expiration')}</label>
<label htmlFor="current_expiration">{t('app.admin.renew_modal.current_expiration')}</label>
<FabInput id="current_expiration"
defaultValue={formatDateTime(subscription.expired_at)}
readOnly />
<label htmlFor="new_start">{t('app.admin.renew_subscription_modal.new_start')}</label>
<label htmlFor="new_start">{t('app.admin.renew_modal.new_start')}</label>
<FabInput id="new_start"
defaultValue={formatDateTime(subscription.expired_at)}
readOnly />
<label htmlFor="new_expiration">{t('app.admin.renew_subscription_modal.new_expiration_date')}</label>
<label htmlFor="new_expiration">{t('app.admin.renew_modal.new_expiration_date')}</label>
<FabInput id="new_expiration"
defaultValue={formatDateTime(expirationDate)}
readOnly/>
@ -135,7 +135,7 @@ const RenewModal: React.FC<RenewModalProps> = ({ isOpen, toggleModal, subscripti
{subscription.plan.monthly_payment && <SelectSchedule show selected={scheduleRequired} onChange={setScheduleRequired} />}
{price?.schedule && <PaymentScheduleSummary schedule={price.schedule as PaymentSchedule} />}
{price && !price?.schedule && <div className="one-go-payment">
<h4>{t('app.admin.renew_subscription_modal.pay_in_one_go')}</h4>
<h4>{t('app.admin.renew_modal.pay_in_one_go')}</h4>
<span>{FormatLib.price(price.price)}</span>
</div>}
</div>

View File

@ -39,7 +39,7 @@ type selectOption = { value: number, label: string };
/**
* Modal dialog shown to create a subscription for the given customer
*/
const SubscribeModal: React.FC<SubscribeModalProps> = ({ isOpen, toggleModal, customer, operator, onError, onSuccess }) => {
export const SubscribeModal: React.FC<SubscribeModalProps> = ({ isOpen, toggleModal, customer, operator, onError, onSuccess }) => {
const { t } = useTranslation('admin');
const [selectedPlan, setSelectedPlan] = useState<Plan>(null);

View File

@ -0,0 +1,43 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { FabModal } from '../base/fab-modal';
import ProofOfIdentityTypeAPI from '../../api/proof-of-identity-type';
interface DeleteSupportingDocumentsTypeModalProps {
isOpen: boolean,
proofOfIdentityTypeId: number,
toggleModal: () => void,
onSuccess: (message: string) => void,
onError: (message: string) => void,
}
/**
* Modal dialog to remove a requested type of supporting documents
*/
export const DeleteSupportingDocumentsTypeModal: React.FC<DeleteSupportingDocumentsTypeModalProps> = ({ isOpen, toggleModal, onSuccess, proofOfIdentityTypeId, onError }) => {
const { t } = useTranslation('admin');
/**
* The user has confirmed the deletion of the requested type of supporting documents
*/
const handleDeleteProofOfIdentityType = async (): Promise<void> => {
try {
await ProofOfIdentityTypeAPI.destroy(proofOfIdentityTypeId);
onSuccess(t('app.admin.settings.account.delete_supporting_documents_type_modal.deleted'));
} catch (e) {
onError(t('app.admin.settings.account.delete_supporting_documents_type_modal.unable_to_delete') + e);
}
};
return (
<FabModal title={t('app.admin.settings.account.delete_supporting_documents_type_modal.confirmation_required')}
isOpen={isOpen}
toggleModal={toggleModal}
closeButton={true}
confirmButton={t('app.admin.settings.account.delete_supporting_documents_type_modal.confirm')}
onConfirm={handleDeleteProofOfIdentityType}
className="delete-supporting-documents-type-modal">
<p>{t('app.admin.settings.account.delete_supporting_documents_type_modal.confirm_delete_supporting_documents_type')}</p>
</FabModal>
);
};

View File

@ -0,0 +1,206 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { react2angular } from 'react2angular';
import _ from 'lodash';
import { HtmlTranslate } from '../base/html-translate';
import { Loader } from '../base/loader';
import { User } from '../../models/user';
import { IApplication } from '../../models/application';
import { ProofOfIdentityType } from '../../models/proof-of-identity-type';
import { ProofOfIdentityFile } from '../../models/proof-of-identity-file';
import ProofOfIdentityTypeAPI from '../../api/proof-of-identity-type';
import ProofOfIdentityFileAPI from '../../api/proof-of-identity-file';
import { IFablab } from '../../models/fablab';
import { FabAlert } from '../base/fab-alert';
import { FabPanel } from '../base/fab-panel';
import { FabButton } from '../base/fab-button';
declare let Fablab: IFablab;
declare const Application: IApplication;
interface SupportingDocumentsFilesProps {
currentUser: User,
onSuccess: (message: string) => void,
onError: (message: string) => void,
}
interface FilesType {
number?: File
}
/**
* This component upload the supporting documents file of the member
*/
export const SupportingDocumentsFiles: React.FC<SupportingDocumentsFilesProps> = ({ currentUser, onSuccess, onError }) => {
const { t } = useTranslation('logged');
const maxProofOfIdentityFileSizeMb = (Fablab.maxProofOfIdentityFileSize / 1024 / 1024).toFixed();
// list of supporting documents type
const [supportingDocumentsTypes, setSupportingDocumentsTypes] = useState<Array<ProofOfIdentityType>>([]);
const [supportingDocumentsFiles, setSupportingDocumentsFiles] = useState<Array<ProofOfIdentityFile>>([]);
const [files, setFiles] = useState<FilesType>({});
const [errors, setErrors] = useState<Array<number>>([]);
// get supporting documents type and files
useEffect(() => {
ProofOfIdentityTypeAPI.index({ group_id: currentUser.group_id }).then(tData => {
setSupportingDocumentsTypes(tData);
});
ProofOfIdentityFileAPI.index({ user_id: currentUser.id }).then(fData => {
setSupportingDocumentsFiles(fData);
});
}, []);
/**
* Return the files matching the given type id
*/
const getSupportingDocumentsFileByType = (supportingDocumentsTypeId: number): ProofOfIdentityFile => {
return _.find<ProofOfIdentityFile>(supportingDocumentsFiles, {
proof_of_identity_type_id: supportingDocumentsTypeId
});
};
/**
* Check if the given type has any uploaded files
*/
const hasFile = (proofOfIdentityTypeId: number): boolean => {
return files[proofOfIdentityTypeId] || getSupportingDocumentsFileByType(proofOfIdentityTypeId);
};
/**
* Check if the current collection of supporting documents types is empty or not.
*/
const hasProofOfIdentityTypes = (): boolean => {
return supportingDocumentsTypes.length > 0;
};
/**
* Callback triggered when a file is selected by the member: check if the file does not exceed the maximum allowed size
*/
const onFileChange = (documentId: number) => {
return (event) => {
const fileSize = event.target.files[0].size;
let _errors: Array<number>;
if (fileSize > Fablab.maxProofOfIdentityFileSize) {
_errors = errors.concat(documentId);
setErrors(_errors);
} else {
_errors = errors.filter(e => e !== documentId);
}
setErrors(_errors);
setFiles({
...files,
[documentId]: event.target.files[0]
});
};
};
/**
* Callback triggered when the user clicks on save: upload the file to the API
*/
const onFileUpload = async () => {
try {
for (const proofOfIdentityTypeId of Object.keys(files)) {
const formData = new FormData();
formData.append('proof_of_identity_file[user_id]', currentUser.id.toString());
formData.append('proof_of_identity_file[proof_of_identity_type_id]', proofOfIdentityTypeId);
formData.append('proof_of_identity_file[attachment]', files[proofOfIdentityTypeId]);
const proofOfIdentityFile = getSupportingDocumentsFileByType(parseInt(proofOfIdentityTypeId, 10));
if (proofOfIdentityFile) {
await ProofOfIdentityFileAPI.update(proofOfIdentityFile.id, formData);
} else {
await ProofOfIdentityFileAPI.create(formData);
}
}
if (Object.keys(files).length > 0) {
ProofOfIdentityFileAPI.index({ user_id: currentUser.id }).then(fData => {
setSupportingDocumentsFiles(fData);
setFiles({});
onSuccess(t('app.logged.dashboard.supporting_documents_files.file_successfully_uploaded'));
});
}
} catch (e) {
onError(t('app.logged.dashboard.supporting_documents_files.unable_to_upload') + e);
}
};
/**
* Return the download URL of the given file
*/
const getSupportingDocumentsFileUrl = (documentId: number) => {
return `/api/proof_of_identity_files/${documentId}/download`;
};
return (
<FabPanel className="supporting-documents-files">
<h3>{t('app.logged.dashboard.supporting_documents_files.supporting_documents_files')}</h3>
<p className="info-area">{t('app.logged.dashboard.supporting_documents_files.my_documents_info')}</p>
<FabAlert level="warning">
<HtmlTranslate trKey="app.logged.dashboard.supporting_documents_files.upload_limits_alert_html"
options={{ SIZE: maxProofOfIdentityFileSizeMb }}/>
</FabAlert>
<div className="files-list">
{supportingDocumentsTypes.map((documentType: ProofOfIdentityType) => {
return (
<div className={`file-item ${errors.includes(documentType.id) ? 'has-error' : ''}`} key={documentType.id}>
<label>{documentType.name}</label>
<div className="fileinput">
<div className="filename-container">
{hasFile(documentType.id) && (
<div>
<i className="fa fa-file fileinput-exists" />
<span className="fileinput-filename">
{files[documentType.id]?.name || getSupportingDocumentsFileByType(documentType.id).attachment}
</span>
</div>
)}
{getSupportingDocumentsFileByType(documentType.id) && !files[documentType.id] && (
<a href={getSupportingDocumentsFileUrl(getSupportingDocumentsFileByType(documentType.id).id)}
target="_blank"
className="file-download"
rel="noreferrer">
<i className="fa fa-download"/>
</a>
)}
</div>
<span className="fileinput-button">
{!hasFile(documentType.id) && (
<span className="fileinput-new">{t('app.logged.dashboard.supporting_documents_files.browse')}</span>
)}
{hasFile(documentType.id) && (
<span className="fileinput-exists">{t('app.logged.dashboard.supporting_documents_files.edit')}</span>
)}
<input type="file"
accept="application/pdf,image/jpeg,image/jpg,image/png"
onChange={onFileChange(documentType.id)}
required />
</span>
</div>
{errors.includes(documentType.id) && <span className="errors-area">
{t('app.logged.dashboard.supporting_documents_files.file_size_error', { SIZE: maxProofOfIdentityFileSizeMb })}
</span>}
</div>
);
})}
</div>
{hasProofOfIdentityTypes() && (
<FabButton className="save-btn" onClick={onFileUpload} disabled={errors.length > 0}>
{t('app.logged.dashboard.supporting_documents_files.save')}
</FabButton>
)}
</FabPanel>
);
};
const SupportingDocumentsFilesWrapper: React.FC<SupportingDocumentsFilesProps> = ({ currentUser, onSuccess, onError }) => {
return (
<Loader>
<SupportingDocumentsFiles currentUser={currentUser} onSuccess={onSuccess} onError={onError} />
</Loader>
);
};
Application.Components.component('supportingDocumentsFiles', react2angular(SupportingDocumentsFilesWrapper, ['currentUser', 'onSuccess', 'onError']));

View File

@ -2,22 +2,22 @@ import React, { BaseSyntheticEvent, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ProofOfIdentityType } from '../../models/proof-of-identity-type';
interface ProofOfIdentityRefusalFormProps {
interface SupportingDocumentsRefusalFormProps {
proofOfIdentityTypes: Array<ProofOfIdentityType>,
onChange: (field: string, value: string | Array<number>) => void,
}
/**
* Form to set the stripe's public and private keys
* Form to set the refuse the uploaded supporting documents
*/
export const ProofOfIdentityRefusalForm: React.FC<ProofOfIdentityRefusalFormProps> = ({ proofOfIdentityTypes, onChange }) => {
export const SupportingDocumentsRefusalForm: React.FC<SupportingDocumentsRefusalFormProps> = ({ proofOfIdentityTypes, onChange }) => {
const { t } = useTranslation('admin');
const [values, setValues] = useState<Array<number>>([]);
const [message, setMessage] = useState<string>('');
/**
* Callback triggered when the name has changed.
* Callback triggered when the message has changed.
*/
const handleMessageChange = (e: BaseSyntheticEvent): void => {
const { value } = e.target;
@ -26,10 +26,9 @@ export const ProofOfIdentityRefusalForm: React.FC<ProofOfIdentityRefusalFormProp
};
/**
* Callback triggered when a checkbox is ticked or unticked.
* This function construct the resulting string, by adding or deleting the provided option identifier.
* Callback triggered when the document type checkbox is ticked or unticked.
*/
const handleProofOfIdnentityTypesChange = (value: number) => {
const handleTypeSelectionChange = (value: number) => {
return (event: BaseSyntheticEvent) => {
let newValues: Array<number>;
if (event.target.checked) {
@ -43,27 +42,32 @@ export const ProofOfIdentityRefusalForm: React.FC<ProofOfIdentityRefusalFormProp
};
/**
* Verify if the provided option is currently ticked (i.e. included in the value string)
* Verify if the provided type is currently ticked (i.e. about to be refused)
*/
const isChecked = (value: number) => {
return values.includes(value);
const isChecked = (typeId: number) => {
return values.includes(typeId);
};
return (
<div className="proof-of-identity-type-form">
<div className="supporting-documents-refusal-form">
<form name="proofOfIdentityRefusalForm">
<div>
{proofOfIdentityTypes.map(type => <div key={type.id} className="">
{proofOfIdentityTypes.map(type => <div key={type.id}>
<label htmlFor={`checkbox-${type.id}`}>{type.name}</label>
<input id={`checkbox-${type.id}`} className="pull-right" type="checkbox" checked={isChecked(type.id)} onChange={handleProofOfIdnentityTypesChange(type.id)} />
<input id={`checkbox-${type.id}`}
type="checkbox"
checked={isChecked(type.id)}
onChange={handleTypeSelectionChange(type.id)} />
</div>)}
</div>
<div className="proof-of-identity-refusal-comment-textarea m-t">
<label htmlFor="proof-of-identity-refusal-comment">{t('app.admin.members_edit.proof_of_identity_refusal_comment')}</label>
<div className="refusal-comment">
<label htmlFor="proof-of-identity-refusal-comment">
{t('app.admin.supporting_documents_refusal_form.refusal_comment')}
</label>
<textarea
id="proof-of-identity-refusal-comment"
value={message}
placeholder={t('app.admin.members_edit.proof_of_identity_refuse_input_message')}
placeholder={t('app.admin.supporting_documents_refusal_form.comment_placeholder')}
onChange={handleMessageChange}
style={{ width: '100%' }}
rows={5}

View File

@ -0,0 +1,75 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FabModal } from '../base/fab-modal';
import { ProofOfIdentityType } from '../../models/proof-of-identity-type';
import { ProofOfIdentityRefusal } from '../../models/proof-of-identity-refusal';
import { User } from '../../models/user';
import ProofOfIdentityRefusalAPI from '../../api/proof-of-identity-refusal';
import { SupportingDocumentsRefusalForm } from './supporting-documents-refusal-form';
interface SupportingDocumentsRefusalModalProps {
isOpen: boolean,
toggleModal: () => void,
onSuccess: (message: string) => void,
onError: (message: string) => void,
proofOfIdentityTypes: Array<ProofOfIdentityType>,
operator: User,
member: User
}
/**
* Modal dialog to notify the member that his documents are refused
*/
export const SupportingDocumentsRefusalModal: React.FC<SupportingDocumentsRefusalModalProps> = ({ isOpen, toggleModal, onSuccess, proofOfIdentityTypes, operator, member, onError }) => {
const { t } = useTranslation('admin');
const [data, setData] = useState<ProofOfIdentityRefusal>({
id: null,
operator_id: operator.id,
user_id: member.id,
proof_of_identity_type_ids: [],
message: ''
});
/**
* Callback triggered when any field has changed in the child form
*/
const handleRefusalChanged = (field: string, value: string | Array<number>) => {
setData({
...data,
[field]: value
});
};
/**
* Save the refusal to the API and send a result message to the parent component
*/
const handleSaveRefusal = async (): Promise<void> => {
try {
await ProofOfIdentityRefusalAPI.create(data);
onSuccess(t('app.admin.supporting_documents_refusal_modal.refusal_successfully_sent'));
} catch (e) {
onError(t('app.admin.supporting_documents_refusal_modal.unable_to_send') + e);
}
};
/**
* Check if the refusal can be saved (i.e. is not empty)
*/
const isPreventedSaveRefusal = (): boolean => {
return !data.message || data.proof_of_identity_type_ids.length === 0;
};
return (
<FabModal title={t('app.admin.supporting_documents_refusal_modal.title')}
isOpen={isOpen}
toggleModal={toggleModal}
closeButton={false}
confirmButton={t('app.admin.supporting_documents_refusal_modal.confirm')}
onConfirm={handleSaveRefusal}
preventConfirm={isPreventedSaveRefusal()}
className="supporting-documents-refusal-modal">
<SupportingDocumentsRefusalForm proofOfIdentityTypes={proofOfIdentityTypes} onChange={handleRefusalChanged}/>
</FabModal>
);
};

View File

@ -5,7 +5,7 @@ import { FabInput } from '../base/fab-input';
import { ProofOfIdentityType } from '../../models/proof-of-identity-type';
import { Group } from '../../models/group';
interface ProofOfIdentityTypeFormProps {
interface SupportingDocumentsTypeFormProps {
groups: Array<Group>,
proofOfIdentityType?: ProofOfIdentityType,
onChange: (field: string, value: string | Array<number>) => void,
@ -18,13 +18,13 @@ interface ProofOfIdentityTypeFormProps {
type selectOption = { value: number, label: string };
/**
* Form to set the stripe's public and private keys
* Form to set create/edit supporting documents type
*/
export const ProofOfIdentityTypeForm: React.FC<ProofOfIdentityTypeFormProps> = ({ groups, proofOfIdentityType, onChange }) => {
export const SupportingDocumentsTypeForm: React.FC<SupportingDocumentsTypeFormProps> = ({ groups, proofOfIdentityType, onChange }) => {
const { t } = useTranslation('admin');
/**
* Convert all themes to the react-select format
* Convert all groups to the react-select format
*/
const buildOptions = (): Array<selectOption> => {
return groups.map(t => {
@ -33,7 +33,7 @@ export const ProofOfIdentityTypeForm: React.FC<ProofOfIdentityTypeFormProps> = (
};
/**
* Return the current groups(s), formatted to match the react-select format
* Return the group(s) associated with the current type, formatted to match the react-select format
*/
const groupsValues = (): Array<selectOption> => {
const res = [];
@ -63,23 +63,23 @@ export const ProofOfIdentityTypeForm: React.FC<ProofOfIdentityTypeFormProps> = (
};
return (
<div className="proof-of-identity-type-form">
<div className="proof-of-identity-type-form-info">
{t('app.admin.settings.compte.proof_of_identity_type_form_info')}
<div className="supporting-documents-type-form">
<div className="info-area">
{t('app.admin.settings.account.supporting_documents_type_form.type_form_info')}
</div>
<form name="proofOfIdentityTypeForm">
<div className="proof-of-identity-type-select m-t">
<div className="field">
<Select defaultValue={groupsValues()}
placeholder={t('app.admin.settings.compte.proof_of_identity_type_select_group')}
placeholder={t('app.admin.settings.account.supporting_documents_type_form.select_group')}
onChange={handleGroupsChange}
options={buildOptions()}
isMulti />
</div>
<div className="proof-of-identity-type-input m-t">
<div className="field">
<FabInput id="proof_of_identity_type_name"
icon={<i className="fa fa-edit" />}
defaultValue={proofOfIdentityType?.name || ''}
placeholder={t('app.admin.settings.compte.proof_of_identity_type_input_name')}
placeholder={t('app.admin.settings.account.supporting_documents_type_form.name')}
onChange={handleNameChange}
debounce={200}
required/>

View File

@ -0,0 +1,79 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { FabModal } from '../base/fab-modal';
import { ProofOfIdentityType } from '../../models/proof-of-identity-type';
import { Group } from '../../models/group';
import ProofOfIdentityTypeAPI from '../../api/proof-of-identity-type';
import { SupportingDocumentsTypeForm } from './supporting-documents-type-form';
interface SupportingDocumentsTypeModalProps {
isOpen: boolean,
toggleModal: () => void,
onSuccess: (message: string) => void,
onError: (message: string) => void,
groups: Array<Group>,
proofOfIdentityType?: ProofOfIdentityType,
}
/**
* Modal dialog to create/edit a supporting documents type
*/
export const SupportingDocumentsTypeModal: React.FC<SupportingDocumentsTypeModalProps> = ({ isOpen, toggleModal, onSuccess, onError, proofOfIdentityType, groups }) => {
const { t } = useTranslation('admin');
const [data, setData] = useState<ProofOfIdentityType>({ id: proofOfIdentityType?.id, group_ids: proofOfIdentityType?.group_ids || [], name: proofOfIdentityType?.name || '' });
useEffect(() => {
setData({ id: proofOfIdentityType?.id, group_ids: proofOfIdentityType?.group_ids || [], name: proofOfIdentityType?.name || '' });
}, [proofOfIdentityType]);
/**
* Callback triggered when an inner form field has changed: updates the internal state accordingly
*/
const handleTypeChanged = (field: string, value: string | Array<number>) => {
setData({
...data,
[field]: value
});
};
/**
* Save the current type to the API
*/
const handleSaveType = async (): Promise<void> => {
try {
if (proofOfIdentityType?.id) {
await ProofOfIdentityTypeAPI.update(data);
onSuccess(t('app.admin.settings.account.supporting_documents_type_modal.successfully_updated'));
} else {
await ProofOfIdentityTypeAPI.create(data);
onSuccess(t('app.admin.settings.account.supporting_documents_type_modal.successfully_created'));
}
} catch (e) {
if (proofOfIdentityType?.id) {
onError(t('app.admin.settings.account.supporting_documents_type_modal.unable_to_update') + e);
} else {
onError(t('app.admin.settings.account.supporting_documents_type_modal.unable_to_create') + e);
}
}
};
/**
* Check if the form is valid (not empty)
*/
const isPreventedSaveType = (): boolean => {
return !data.name || data.group_ids.length === 0;
};
return (
<FabModal title={t(`app.admin.settings.account.supporting_documents_type_modal.${proofOfIdentityType ? 'edit' : 'new'}_type`)}
isOpen={isOpen}
toggleModal={toggleModal}
closeButton={false}
confirmButton={t(`app.admin.settings.account.supporting_documents_type_modal.${proofOfIdentityType ? 'edit' : 'create'}`)}
onConfirm={handleSaveType}
preventConfirm={isPreventedSaveType()}>
<SupportingDocumentsTypeForm proofOfIdentityType={proofOfIdentityType} groups={groups} onChange={handleTypeChanged}/>
</FabModal>
);
};

View File

@ -0,0 +1,279 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { react2angular } from 'react2angular';
import _ from 'lodash';
import { HtmlTranslate } from '../base/html-translate';
import { Loader } from '../base/loader';
import { IApplication } from '../../models/application';
import { ProofOfIdentityType } from '../../models/proof-of-identity-type';
import { Group } from '../../models/group';
import { SupportingDocumentsTypeModal } from './supporting-documents-type-modal';
import { DeleteSupportingDocumentsTypeModal } from './delete-supporting-documents-type-modal';
import GroupAPI from '../../api/group';
import ProofOfIdentityTypeAPI from '../../api/proof-of-identity-type';
import { FabPanel } from '../base/fab-panel';
import { FabAlert } from '../base/fab-alert';
import { FabButton } from '../base/fab-button';
declare const Application: IApplication;
interface SupportingDocumentsTypesListProps {
onSuccess: (message: string) => void,
onError: (message: string) => void,
}
/**
* This component shows a list of all types of supporting documents (e.g. student ID, Kbis extract, etc.)
*/
const SupportingDocumentsTypesList: React.FC<SupportingDocumentsTypesListProps> = ({ onSuccess, onError }) => {
const { t } = useTranslation('admin');
// list of displayed supporting documents type
const [supportingDocumentsTypes, setSupportingDocumentsTypes] = useState<Array<ProofOfIdentityType>>([]);
// currently added/edited type
const [supportingDocumentsType, setSupportingDocumentsType] = useState<ProofOfIdentityType>(null);
// list ordering
const [supportingDocumentsTypeOrder, setSupportingDocumentsTypeOrder] = useState<string>(null);
// creation/edition modal
const [modalIsOpen, setModalIsOpen] = useState<boolean>(false);
// all groups
const [groups, setGroups] = useState<Array<Group>>([]);
// deletion modal
const [destroyModalIsOpen, setDestroyModalIsOpen] = useState<boolean>(false);
// currently deleted type
const [supportingDocumentsTypeId, setSupportingDocumentsTypeId] = useState<number>(null);
// get groups
useEffect(() => {
GroupAPI.index({ disabled: false, admins: false }).then(data => {
setGroups(data);
ProofOfIdentityTypeAPI.index().then(pData => {
setSupportingDocumentsTypes(pData);
});
});
}, []);
/**
* Check if the current collection of supporting documents types is empty or not.
*/
const hasTypes = (): boolean => {
return supportingDocumentsTypes.length > 0;
};
/**
* Init the process of creating a new supporting documents type
*/
const addType = (): void => {
setSupportingDocumentsType(null);
setModalIsOpen(true);
};
/**
* Init the process of editing the given type
*/
const editType = (type: ProofOfIdentityType): () => void => {
return (): void => {
setSupportingDocumentsType(type);
setModalIsOpen(true);
};
};
/**
* Toggle the modal dialog to create/edit a type
*/
const toggleCreateAndEditModal = (): void => {
setModalIsOpen(!modalIsOpen);
};
/**
* Callback triggred when the current type was successfully saved
*/
const onSaveTypeSuccess = (message: string): void => {
setModalIsOpen(false);
ProofOfIdentityTypeAPI.index().then(pData => {
setSupportingDocumentsTypes(orderTypes(pData, supportingDocumentsTypeOrder));
onSuccess(message);
}).catch((error) => {
onError('Unable to load proof of identity types' + error);
});
};
/**
* Init the process of deleting a supporting documents type (ask for confirmation)
*/
const destroyType = (id: number): () => void => {
return (): void => {
setSupportingDocumentsTypeId(id);
setDestroyModalIsOpen(true);
};
};
/**
* Open/closes the confirmation before deletion modal
*/
const toggleDestroyModal = (): void => {
setDestroyModalIsOpen(!destroyModalIsOpen);
};
/**
* Callback triggred when the current type was successfully deleted
*/
const onDestroySuccess = (message: string): void => {
setDestroyModalIsOpen(false);
ProofOfIdentityTypeAPI.index().then(pData => {
setSupportingDocumentsTypes(pData);
setSupportingDocumentsTypes(orderTypes(pData, supportingDocumentsTypeOrder));
onSuccess(message);
}).catch((error) => {
onError('Unable to load proof of identity types' + error);
});
};
/**
* Change the list ordering, according to the provided key
*/
const setTypeOrder = (orderBy: string): () => void => {
return () => {
let order = orderBy;
if (supportingDocumentsTypeOrder === orderBy) {
order = `-${orderBy}`;
}
setSupportingDocumentsTypeOrder(order);
setSupportingDocumentsTypes(orderTypes(supportingDocumentsTypes, order));
};
};
/**
* Sort the provided types according to the provided ordering key and return the resulting list
*/
const orderTypes = (types: Array<ProofOfIdentityType>, orderBy?: string): Array<ProofOfIdentityType> => {
if (!orderBy) {
return types;
}
const order = orderBy[0] === '-' ? 'desc' : 'asc';
if (orderBy.search('group_name') !== -1) {
return _.orderBy(types, (type: ProofOfIdentityType) => getGroupsNames(type.group_ids), order);
} else {
return _.orderBy(types, 'name', order);
}
};
/**
* Return the icon classes to use, according to the provided ordering key
*/
const orderClassName = (orderBy: string): string => {
if (supportingDocumentsTypeOrder) {
const order = supportingDocumentsTypeOrder[0] === '-' ? supportingDocumentsTypeOrder.substr(1) : supportingDocumentsTypeOrder;
if (order === orderBy) {
return `fa fa-arrows-v ${supportingDocumentsTypeOrder[0] === '-' ? 'fa-sort-alpha-desc' : 'fa-sort-alpha-asc'}`;
}
}
return 'fa fa-arrows-v';
};
/**
* Return a comma separated list of the names of the provided groups
*/
const getGroupsNames = (groupIds: Array<number>): string => {
if (groupIds.length === groups.length && groupIds.length > 0) {
return t('app.admin.settings.account.supporting_documents_types_list.all_groups');
}
const _groups = _.filter(groups, (g: Group) => { return groupIds.includes(g.id); });
return _groups.map((g: Group) => g.name).join(', ');
};
/**
* Redirect the user to the new group page
*/
const addGroup = (): void => {
window.location.href = '/#!/admin/members?tabs=1';
};
return (
<FabPanel className="supporting-documents-types-list" header={<div>
<span>{t('app.admin.settings.account.supporting_documents_types_list.add_supporting_documents_types')}</span>
</div>}>
<div className="types-list">
<div className="groups">
<p>{t('app.admin.settings.account.supporting_documents_types_list.supporting_documents_type_info')}</p>
<FabAlert level="warning">
<HtmlTranslate trKey="app.admin.settings.account.supporting_documents_types_list.no_groups_info" />
<FabButton onClick={addGroup}>{t('app.admin.settings.account.supporting_documents_types_list.create_groups')}</FabButton>
</FabAlert>
</div>
<div className="title">
<h3>{t('app.admin.settings.account.supporting_documents_types_list.supporting_documents_type_title')}</h3>
<FabButton onClick={addType}>{t('app.admin.settings.account.supporting_documents_types_list.add_type')}</FabButton>
</div>
<SupportingDocumentsTypeModal isOpen={modalIsOpen}
groups={groups}
proofOfIdentityType={supportingDocumentsType}
toggleModal={toggleCreateAndEditModal}
onSuccess={onSaveTypeSuccess}
onError={onError} />
<DeleteSupportingDocumentsTypeModal isOpen={destroyModalIsOpen}
proofOfIdentityTypeId={supportingDocumentsTypeId}
toggleModal={toggleDestroyModal}
onSuccess={onDestroySuccess}
onError={onError}/>
<table>
<thead>
<tr>
<th className="group-name">
<a onClick={setTypeOrder('group_name')}>
{t('app.admin.settings.account.supporting_documents_types_list.group_name')}
<i className={orderClassName('group_name')} />
</a>
</th>
<th className="name">
<a onClick={setTypeOrder('name')}>
{t('app.admin.settings.account.supporting_documents_types_list.name')}
<i className={orderClassName('name')} />
</a>
</th>
<th className="actions"></th>
</tr>
</thead>
<tbody>
{supportingDocumentsTypes.map(poit => {
return (
<tr key={poit.id}>
<td>{getGroupsNames(poit.group_ids)}</td>
<td>{poit.name}</td>
<td>
<div className="buttons">
<FabButton className="edit-btn" onClick={editType(poit)}>
<i className="fa fa-edit" />
</FabButton>
<FabButton className="delete-btn" onClick={destroyType(poit.id)}>
<i className="fa fa-trash" />
</FabButton>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
{!hasTypes() && (
<p className="no-types-info">
<HtmlTranslate trKey="app.admin.settings.account.supporting_documents_types_list.no_types" />
</p>
)}
</div>
</FabPanel>
);
};
const SupportingDocumentsTypesListWrapper: React.FC<SupportingDocumentsTypesListProps> = (props) => {
return (
<Loader>
<SupportingDocumentsTypesList {...props} />
</Loader>
);
};
Application.Components.component('supportingDocumentsTypesList', react2angular(SupportingDocumentsTypesListWrapper, ['onSuccess', 'onError']));

View File

@ -0,0 +1,133 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { react2angular } from 'react2angular';
import _ from 'lodash';
import { Loader } from '../base/loader';
import { User } from '../../models/user';
import { IApplication } from '../../models/application';
import { ProofOfIdentityType } from '../../models/proof-of-identity-type';
import { ProofOfIdentityFile } from '../../models/proof-of-identity-file';
import ProofOfIdentityTypeAPI from '../../api/proof-of-identity-type';
import ProofOfIdentityFileAPI from '../../api/proof-of-identity-file';
import { SupportingDocumentsRefusalModal } from './supporting-documents-refusal-modal';
import { FabButton } from '../base/fab-button';
import { FabPanel } from '../base/fab-panel';
declare const Application: IApplication;
interface SupportingDocumentsValidationProps {
operator: User,
member: User
onSuccess: (message: string) => void,
onError: (message: string) => void,
}
/**
* This component shows a list of supporting documents file of member, admin can download and valid
**/
const SupportingDocumentsValidation: React.FC<SupportingDocumentsValidationProps> = ({ operator, member, onSuccess, onError }) => {
const { t } = useTranslation('admin');
// list of supporting documents type
const [documentsTypes, setDocumentsTypes] = useState<Array<ProofOfIdentityType>>([]);
const [documentsFiles, setDocumentsFiles] = useState<Array<ProofOfIdentityFile>>([]);
const [modalIsOpen, setModalIsOpen] = useState<boolean>(false);
// get groups
useEffect(() => {
ProofOfIdentityTypeAPI.index({ group_id: member.group_id }).then(tData => {
setDocumentsTypes(tData);
});
ProofOfIdentityFileAPI.index({ user_id: member.id }).then(fData => {
setDocumentsFiles(fData);
});
}, []);
/**
* Return the file associated with the provided type
*/
const getFileByType = (typeId: number): ProofOfIdentityFile => {
return _.find<ProofOfIdentityFile>(documentsFiles, { proof_of_identity_type_id: typeId });
};
/**
* Check if any supporting documents types has been defined.
*/
const hasSupportingDocumentsTypes = (): boolean => {
return documentsTypes.length > 0;
};
/**
* Return the download URL of the given file
*/
const getProofOfIdentityFileUrl = (documentId: number): string => {
return `/api/proof_of_identity_files/${documentId}/download`;
};
/**
* Open/closes the modal dialog to refuse the documents
*/
const toggleModal = (): void => {
setModalIsOpen(!modalIsOpen);
};
/**
* Callback triggered when the refusal was successfully saved
*/
const onSaveRefusalSuccess = (message: string): void => {
setModalIsOpen(false);
onSuccess(message);
};
return (
<div className="supporting-documents-validation">
<FabPanel>
<h3>{t('app.admin.supporting_documents_validation.title')}</h3>
<p className="info-area">{t('app.admin.supporting_documents_validation.find_below_documents_files')}</p>
{documentsTypes.map((documentType: ProofOfIdentityType) => {
return (
<div key={documentType.id} className="document-type">
<div className="type-name">{documentType.name}</div>
{getFileByType(documentType.id) && (
<a href={getProofOfIdentityFileUrl(getFileByType(documentType.id).id)} target="_blank" rel="noreferrer">
<span className="filename">{getFileByType(documentType.id).attachment}</span>
<i className="fa fa-download"></i>
</a>
)}
{!getFileByType(documentType.id) && (
<div className="missing-file">{t('app.admin.supporting_documents_validation.to_complete')}</div>
)}
</div>
);
})}
</FabPanel>
{hasSupportingDocumentsTypes() && !member.validated_at && (
<FabPanel className="refusal">
<h3>{t('app.admin.supporting_documents_validation.refuse_documents')}</h3>
<p className="text-black">{t('app.admin.supporting_documents_validation.refuse_documents_info')}</p>
<FabButton className="refuse-btn" onClick={toggleModal}>{t('app.admin.supporting_documents_validation.refuse_documents')}</FabButton>
<SupportingDocumentsRefusalModal
isOpen={modalIsOpen}
proofOfIdentityTypes={documentsTypes}
toggleModal={toggleModal}
operator={operator}
member={member}
onError={onError}
onSuccess={onSaveRefusalSuccess}/>
</FabPanel>
)}
</div>
);
};
const SupportingDocumentsValidationWrapper: React.FC<SupportingDocumentsValidationProps> = (props) => {
return (
<Loader>
<SupportingDocumentsValidation {...props} />
</Loader>
);
};
export { SupportingDocumentsValidationWrapper as SupportingDocumentsValidation };
Application.Components.component('supportingDocumentsValidation', react2angular(SupportingDocumentsValidationWrapper, ['operator', 'member', 'onSuccess', 'onError']));

View File

@ -28,6 +28,8 @@ import TagAPI from '../../api/tag';
import { FormMultiSelect } from '../form/form-multi-select';
import ProfileCustomFieldAPI from '../../api/profile-custom-field';
import { ProfileCustomField } from '../../models/profile-custom-field';
import { SettingName } from '../../models/setting';
import SettingAPI from '../../api/setting';
declare const Application: IApplication;
@ -68,6 +70,7 @@ export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size,
const [groups, setGroups] = useState<selectOption[]>([]);
const [termsAndConditions, setTermsAndConditions] = useState<CustomAsset>(null);
const [profileCustomFields, setProfileCustomFields] = useState<ProfileCustomField[]>([]);
const [requiredFieldsSettings, setRequiredFieldsSettings] = useState<Map<SettingName, string>>(new Map());
useEffect(() => {
AuthProviderAPI.active().then(data => {
@ -94,6 +97,9 @@ export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size,
});
setValue('invoicing_profile_attributes.user_profile_custom_fields_attributes', userProfileCustomFields);
}).catch(error => onError(error));
SettingAPI.query([SettingName.PhoneRequired, SettingName.AddressRequired])
.then(settings => setRequiredFieldsSettings(settings))
.catch(error => onError(error));
}, []);
/**
@ -202,6 +208,7 @@ export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size,
register={register}
label={t('app.shared.user_profile_form.date_of_birth')}
disabled={isDisabled}
rules={{ required: true }}
type="date" />
<FormInput id="profile_attributes.phone"
register={register}
@ -209,7 +216,8 @@ export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size,
pattern: {
value: phoneRegex,
message: t('app.shared.user_profile_form.phone_number_invalid')
}
},
required: requiredFieldsSettings.get(SettingName.PhoneRequired) === 'true'
}}
disabled={isDisabled}
formState={formState}
@ -222,6 +230,7 @@ export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size,
<FormInput id="invoicing_profile_attributes.address_attributes.address"
register={register}
disabled={isDisabled}
rules={{ required: requiredFieldsSettings.get(SettingName.AddressRequired) === 'true' }}
label={t('app.shared.user_profile_form.address')} />
</div>
</div>

View File

@ -6,6 +6,7 @@ import { User } from '../../models/user';
import { IApplication } from '../../models/application';
import { react2angular } from 'react2angular';
import MemberAPI from '../../api/member';
import { TDateISO } from '../../typings/date-iso';
declare const Application: IApplication;
@ -34,23 +35,23 @@ export const UserValidation: React.FC<UserValidationProps> = ({ member, onSucces
setValue(_value);
const _member = _.clone(member);
if (_value) {
_member.validated_at = new Date();
_member.validated_at = new Date().toISOString() as TDateISO;
} else {
_member.validated_at = null;
}
MemberAPI.validate(_member)
.then((user: User) => {
onSuccess(user, t(`app.admin.members_edit.${_value ? 'validate' : 'invalidate'}_member_success`));
onSuccess(user, t(`app.admin.user_validation.${_value ? 'validate' : 'invalidate'}_member_success`));
}).catch(err => {
setValue(!_value);
onError(t(`app.admin.members_edit.${_value ? 'validate' : 'invalidate'}_member_error`) + err);
onError(t(`app.admin.user_validation.${_value ? 'validate' : 'invalidate'}_member_error`) + err);
});
};
return (
<div className="user-validation">
<label htmlFor="user-validation-switch" className="control-label m-r">{t('app.admin.members_edit.validate_account')}</label>
<Switch checked={value} id="user-validation-switch" onChange={handleChanged} className="v-middle"></Switch>
<label htmlFor="user-validation-switch">{t('app.admin.user_validation.validate_account')}</label>
<Switch checked={value} id="user-validation-switch" onChange={handleChanged} className="switch"></Switch>
</div>
);
};

View File

@ -929,9 +929,6 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
const initialize = function () {
CSRF.setMetaTags();
// init the birthdate to JS object
$scope.user.statistic_profile_attributes.birthday = moment($scope.user.statistic_profile_attributes.birthday).toDate();
// the user subscription
if (($scope.user.subscribed_plan != null) && ($scope.user.subscription != null)) {
$scope.subscription = $scope.user.subscription;

View File

@ -72,6 +72,7 @@ Application.Controllers.controller('EventsController', ['$scope', '$state', 'Eve
// reinitialize results datasets
$scope.page = 1;
$scope.eventsGroupByMonth = {};
$scope.featuredEevent = null;
$scope.events = [];
$scope.monthOrder = [];
$scope.noMoreResults = false;
@ -94,6 +95,16 @@ Application.Controllers.controller('EventsController', ['$scope', '$state', 'Eve
*/
$scope.onSingleDay = function (event) { moment(event.start_date).isSame(event.end_date, 'day'); };
/**
* Move down the viewport to the featured event
*/
$scope.scrollToFeaturedEvent = function () {
const card = document.getElementsByClassName('featured-event')[0];
if (card) {
card.childNodes[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
/* PRIVATE SCOPE */
/**
@ -117,7 +128,8 @@ Application.Controllers.controller('EventsController', ['$scope', '$state', 'Eve
});
});
$scope.eventsGroupByMonth = Object.assign($scope.eventsGroupByMonth, eventsGroupedByMonth);
return $scope.monthOrder = Object.keys($scope.eventsGroupByMonth);
$scope.monthOrder = Object.keys($scope.eventsGroupByMonth);
$scope.featuredEevent = _.minBy(events.filter(e => moment(e.start_date).isAfter()), e => e.start_date);
}
};

View File

@ -21,6 +21,10 @@ Application.Directives.directive('textSetting', ['Setting', 'growl', '_t',
if (typeof $scope.type === 'undefined') {
$scope.type = 'text';
}
// 'required' default to true
if (typeof $scope.required === 'undefined') {
$scope.required = true;
}
// The setting
$scope.setting = {
name: $scope.name,

View File

@ -350,3 +350,12 @@ Application.Filters.filter('filterDisabled', [function () {
}
};
}]);
Application.Filters.filter('currency', [function ($locale) {
return function (amount) {
// if null or undefined pass it through
return (amount == null)
? amount
: new Intl.NumberFormat(Fablab.intl_locale, { style: 'currency', currency: Fablab.intl_currency }).format(amount);
};
}]);

View File

@ -14,6 +14,7 @@ export type mappingType = 'string' | 'text' | 'date' | 'integer' | 'boolean';
export interface AuthenticationProviderMapping {
id?: number,
_destroy?: boolean,
local_model: 'user' | 'profile',
local_field: string,
api_field: string,

View File

@ -4,8 +4,8 @@ export interface ProofOfIdentityFileIndexFilter {
}
export interface ProofOfIdentityFile {
id: number,
attachment: string,
user_id: number,
proof_of_identity_file_id: number,
id?: number,
attachment?: string,
user_id?: number,
proof_of_identity_type_id: number,
}

View File

@ -121,5 +121,6 @@ export const UserFieldMapping = Object.assign({
'profile_attributes.interest': 'profile.interest',
'profile_attributes.software_mastered': 'profile.software_mastered',
is_allow_contact: 'user.is_allow_contact',
is_allow_newsletter: 'user.is_allow_newsletter'
is_allow_newsletter: 'user.is_allow_newsletter',
group_id: 'user.group_id'
}, ...socialMappings);

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