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, "$": true,
"KeyboardEvent": true "KeyboardEvent": true
}, },
"plugins": ["html-erb"], "plugins": ["html-erb", "fabmanager"],
"overrides": [ "overrides": [
{ {
"files": ["**/*.ts", "**/*.tsx"], "files": ["**/*.ts", "**/*.tsx"],
@ -46,6 +46,20 @@
"react/prop-types": "off" "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"], "files": ["app/frontend/src/javascript/models/**/*.ts"],
"rules": { "rules": {

View File

@ -2,6 +2,39 @@
## next deploy ## 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 ## v5.4.4 2022 June 8
- Check shopping cart items are valid before online payment - 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 # Install gems in a cache efficient way
WORKDIR /tmp WORKDIR /tmp
COPY Gemfile /tmp/ COPY Gemfile* /tmp/
COPY Gemfile.lock /tmp/
RUN bundle config set --local without 'development test doc' && bundle install && bundle binstubs --all RUN bundle config set --local without 'development test doc' && bundle install && bundle binstubs --all
# Prepare the application directories # Prepare the application directories
RUN mkdir -p /var/log/supervisor && \ RUN mkdir -p /var/log/supervisor && \
mkdir -p /usr/src/app/tmp/sockets && \ 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 # Install Javascript packages
WORKDIR /usr/src/app WORKDIR /usr/src/app
@ -65,23 +70,24 @@ RUN apk del .build-deps && \
rm -rf /tmp/* \ rm -rf /tmp/* \
/var/tmp/* \ /var/tmp/* \
/var/cache/apk/* \ /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 source files
COPY docker/database.yml /usr/src/app/config/database.yml COPY docker/database.yml /usr/src/app/config/database.yml
COPY . /usr/src/app COPY . /usr/src/app
# Volumes (the folders are created by setup.sh) # Volumes (the folders are created by setup.sh)
VOLUME /usr/src/app/invoices VOLUME /usr/src/app/invoices \
VOLUME /usr/src/app/payment_schedules /usr/src/app/payment_schedules \
VOLUME /usr/src/app/exports /usr/src/app/exports \
VOLUME /usr/src/app/imports /usr/src/app/imports \
VOLUME /usr/src/app/public /usr/src/app/public \
VOLUME /usr/src/app/public/uploads /usr/src/app/public/uploads \
VOLUME /usr/src/app/public/packs /usr/src/app/public/packs \
VOLUME /usr/src/app/accounting /usr/src/app/accounting \
VOLUME /usr/src/app/proof_of_identity_files /usr/src/app/proof_of_identity_files \
VOLUME /var/log/supervisor /var/log/supervisor
# Expose port 3000 to the Docker host, so we can access it from the outside # Expose port 3000 to the Docker host, so we can access it from the outside
EXPOSE 3000 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 worker: bundle exec sidekiq -C ./config/sidekiq.yml
webpack: bin/webpacker-dev-server webpack: bin/webpacker-dev-server

View File

@ -267,7 +267,7 @@ class API::MembersController < API::ApiController
:dailymotion, :github, :echosciences, :pinterest, :lastfm, :flickr, :dailymotion, :github, :echosciences, :pinterest, :lastfm, :flickr,
user_avatar_attributes: %i[id attachment destroy]], user_avatar_attributes: %i[id attachment destroy]],
invoicing_profile_attributes: [ invoicing_profile_attributes: [
:id, :id, :organization,
address_attributes: %i[id address], address_attributes: %i[id address],
organization_attributes: [:id, :name, 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] 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, :dailymotion, :github, :echosciences, :pinterest, :lastfm, :flickr,
user_avatar_attributes: %i[id attachment destroy]], user_avatar_attributes: %i[id attachment destroy]],
invoicing_profile_attributes: [ invoicing_profile_attributes: [
:id, :id, :organization,
address_attributes: %i[id address], address_attributes: %i[id address],
organization_attributes: [:id, :name, 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] 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. - 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>`. - 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. - 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 Switch from 'react-switch';
import { react2angular } from 'react2angular'; import { react2angular } from 'react2angular';

View File

@ -1,9 +1,9 @@
import React, { useEffect, useState } from 'react'; 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 { FieldValues } from 'react-hook-form/dist/types/fields';
import AuthProviderAPI from '../../api/auth-provider'; import AuthProviderAPI from '../../api/auth-provider';
import { AuthenticationProviderMapping, MappingFields, mappingType, ProvidableType } from '../../models/authentication-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 { FormSelect } from '../form/form-select';
import { FormInput } from '../form/form-input'; import { FormInput } from '../form/form-input';
import { useTranslation } from 'react-i18next'; 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 // fetch the mapping data from the API on mount
useEffect(() => { useEffect(() => {
AuthProviderAPI.mappingFields().then((data) => { AuthProviderAPI.mappingFields().then((data) => {
@ -114,7 +139,7 @@ export const DataMappingForm = <TFieldValues extends FieldValues, TContext exten
</FabButton> </FabButton>
</div> </div>
{fields.map((item, index) => ( {fields.map((item, index) => (
<div key={item.id} className="mapping-item"> <div key={item.id} className={`mapping-item ${itemStatus(index)}`}>
<div className="inputs"> <div className="inputs">
<FormInput id={`auth_provider_mappings_attributes.${index}.id`} register={register} type="hidden" /> <FormInput id={`auth_provider_mappings_attributes.${index}.id`} register={register} type="hidden" />
<div className="local-data"> <div className="local-data">
@ -141,7 +166,7 @@ export const DataMappingForm = <TFieldValues extends FieldValues, TContext exten
onClick={toggleTypeMappingModal(index)} onClick={toggleTypeMappingModal(index)}
disabled={getField(output, index) === undefined} disabled={getField(output, index) === undefined}
tooltip={t('app.admin.authentication.data_mapping_form.data_mapping')} /> 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)} <TypeMappingModal model={getModel(output, index)}
field={getField(output, index)} field={getField(output, index)}
type={getDataType(output, index)} type={getDataType(output, index)}

View File

@ -13,6 +13,10 @@ interface Oauth2DataMappingFormProps<TFieldValues, TContext extends object> {
index: number, 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>) => { export const Oauth2DataMappingForm = <TFieldValues extends FieldValues, TContext extends object>({ register, control, index }: Oauth2DataMappingFormProps<TFieldValues, TContext>) => {
const { t } = useTranslation('admin'); const { t } = useTranslation('admin');

View File

@ -16,6 +16,10 @@ interface OpenidConnectDataMappingFormProps<TFieldValues> {
index: number, 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>) => { export const OpenidConnectDataMappingForm = <TFieldValues extends FieldValues>({ register, setValue, currentFormValues, index }: OpenidConnectDataMappingFormProps<TFieldValues>) => {
const { t } = useTranslation('admin'); const { t } = useTranslation('admin');
@ -40,17 +44,19 @@ export const OpenidConnectDataMappingForm = <TFieldValues extends FieldValues>({
const model = currentFormValues[index]?.local_model; const model = currentFormValues[index]?.local_model;
const field = currentFormValues[index]?.local_field; const field = currentFormValues[index]?.local_field;
const configuration = standardConfiguration[`${model}.${field}`]; const configuration = standardConfiguration[`${model}.${field}`];
setValue( if (configuration) {
`auth_provider_mappings_attributes.${index}.api_field` as Path<TFieldValues>, setValue(
configuration.api_field as UnpackNestedValue<FieldPathValue<TFieldValues, Path<TFieldValues>>> `auth_provider_mappings_attributes.${index}.api_field` as Path<TFieldValues>,
); configuration.api_field as UnpackNestedValue<FieldPathValue<TFieldValues, Path<TFieldValues>>>
if (configuration.transformation) { );
Object.keys(configuration.transformation).forEach((key) => { if (configuration.transformation) {
setValue( Object.keys(configuration.transformation).forEach((key) => {
`auth_provider_mappings_attributes.${index}.transformation.${key}` as Path<TFieldValues>, setValue(
configuration.transformation[key] as UnpackNestedValue<FieldPathValue<TFieldValues, Path<TFieldValues>>> `auth_provider_mappings_attributes.${index}.transformation.${key}` as Path<TFieldValues>,
); configuration.transformation[key] as UnpackNestedValue<FieldPathValue<TFieldValues, Path<TFieldValues>>>
}); );
});
}
} }
}; };

View File

@ -20,6 +20,9 @@ interface OpenidConnectFormProps<TFieldValues, TContext extends object> {
setValue: UseFormSetValue<TFieldValues>, 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>) => { export const OpenidConnectForm = <TFieldValues extends FieldValues, TContext extends object>({ register, control, currentFormValues, formState, setValue }: OpenidConnectFormProps<TFieldValues, TContext>) => {
const { t } = useTranslation('admin'); const { t } = useTranslation('admin');

View File

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

View File

@ -45,7 +45,7 @@ export const FabModal: React.FC<FabModalProps> = ({ title, isOpen, toggleModal,
className={`fab-modal fab-modal-${width} ${className}`} className={`fab-modal fab-modal-${width} ${className}`}
overlayClassName="fab-modal-overlay" overlayClassName="fab-modal-overlay"
onRequestClose={toggleModal}> 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"> <div className="fab-modal-header">
{!customHeader && <h1>{ title }</h1>} {!customHeader && <h1>{ title }</h1>}
{customHeader && customHeader} {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 }) => { export const LabelledInput: React.FC<LabelledInputProps> = ({ id, type, label, value, onChange }) => {
return ( return (
<div className="input-with-label"> <div className="labelled-input">
<label className="label" htmlFor={id}>{label}</label> <label htmlFor={id}>{label}</label>
<input className="input" id={id} type={type} value={value} onChange={onChange} /> <input id={id} type={type} value={value} onChange={onChange} />
</div> </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>) => { 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 { 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 // TODO: Add ctrl+click on link to visit
const editorRef: React.MutableRefObject<Editor | null> = useRef(null); const editorRef: React.MutableRefObject<Editor | null> = useRef(null);
@ -66,7 +66,7 @@ export const FabTextEditor: React.ForwardRefRenderFunction<FabTextEditorRef, Fab
Iframe, Iframe,
Image.configure({ Image.configure({
HTMLAttributes: { HTMLAttributes: {
class: 'fab-textEditor-image' class: 'fab-text-editor-image'
} }
}) })
], ],
@ -85,14 +85,14 @@ export const FabTextEditor: React.ForwardRefRenderFunction<FabTextEditorRef, Fab
editorRef.current = editor; editorRef.current = editor;
return ( 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} /> <MenuBar editor={editor} paragraphTools={paragraphTools} video={video} image={image} disabled={disabled} />
<EditorContent editor={editor} /> <EditorContent editor={editor} />
<div className="fab-textEditor-character-count"> <div className="fab-text-editor-character-count">
{editor?.storage.characterCount.characters()} / {limit} {editor?.storage.characterCount.characters()} / {limit}
</div> </div>
{error && {error &&
<div className="fab-textEditor-error"> <div className="fab-text-editor-error">
<WarningOctagon size={24} /> <WarningOctagon size={24} />
<p className="">{error}</p> <p className="">{error}</p>
</div> </div>
@ -101,4 +101,5 @@ export const FabTextEditor: React.ForwardRefRenderFunction<FabTextEditorRef, Fab
); );
}; };
// eslint-disable-next-line import/no-default-export
export default forwardRef(FabTextEditor); 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>({ export default Node.create<IframeOptions>({
name: 'iframe', name: 'iframe',
@ -29,7 +30,7 @@ export default Node.create<IframeOptions>({
return { return {
allowFullscreen: true, allowFullscreen: true,
HTMLAttributes: { 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 ( return (
<> <>
<div className={`fab-textEditor-menu ${disabled ? 'fab-textEditor-menu--disabled' : ''}`}> <div className={`fab-text-editor-menu ${disabled ? 'fab-text-editor-menu--disabled' : ''}`}>
{ paragraphTools && { paragraphTools &&
(<> (<>
<button <button
@ -168,7 +168,7 @@ export const MenuBar: React.FC<MenuBarProps> = ({ editor, paragraphTools, video,
> >
<Quotes size={24} /> <Quotes size={24} />
</button> </button>
<span className='divider'></span> <span className='menu-divider'></span>
</>) </>)
} }
<button <button
@ -203,7 +203,7 @@ export const MenuBar: React.FC<MenuBarProps> = ({ editor, paragraphTools, video,
> >
<LinkSimpleHorizontal size={24} /> <LinkSimpleHorizontal size={24} />
</button> </button>
{ (video || image) && <span className='divider'></span> } { (video || image) && <span className='menu-divider'></span> }
{ video && { video &&
(<> (<>
<button <button
@ -228,19 +228,19 @@ export const MenuBar: React.FC<MenuBarProps> = ({ editor, paragraphTools, video,
} }
</div> </div>
<div ref={ref} className={`fab-textEditor-subMenu ${submenu ? 'is-active' : ''}`}> <div ref={ref} className={`fab-text-editor-subMenu ${submenu ? 'is-active' : ''}`}>
{ submenu === 'link' && { submenu === 'link' &&
(<> (<>
<h6>{t('app.shared.text_editor.add_link')}</h6> <h6>{t('app.shared.text_editor.menu_bar.add_link')}</h6>
<div> <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}> <button type='button' onClick={unsetLink}>
<Trash size={24} /> <Trash size={24} />
</button> </button>
</div> </div>
<div> <div>
<label className='tab'> <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'} /> <input type="checkbox" onChange={toggleTarget} checked={url.target === '_blank'} />
<span className='switch'></span> <span className='switch'></span>
</label> </label>
@ -252,14 +252,14 @@ export const MenuBar: React.FC<MenuBarProps> = ({ editor, paragraphTools, video,
} }
{ submenu === '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}> <select name="provider" onChange={handleSelect}>
<option value="youtube">YouTube</option> <option value="youtube">YouTube</option>
<option value="vimeo">Vimeo</option> <option value="vimeo">Vimeo</option>
<option value="dailymotion">Dailymotion</option> <option value="dailymotion">Dailymotion</option>
</select> </select>
<div> <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()}> <button type='button' onClick={() => addIframe()}>
<CheckCircle size={24} /> <CheckCircle size={24} />
</button> </button>
@ -268,9 +268,9 @@ export const MenuBar: React.FC<MenuBarProps> = ({ editor, paragraphTools, video,
} }
{ submenu === 'image' && { submenu === 'image' &&
(<> (<>
<h6>{t('app.shared.text_editor.add_image')}</h6> <h6>{t('app.shared.text_editor.menu_bar.add_image')}</h6>
<div> <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()}> <button type='button' onClick={() => addImage()}>
<CheckCircle size={24} /> <CheckCircle size={24} />
</button> </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 { useTranslation } from 'react-i18next';
import Select from 'react-select'; import Select from 'react-select';
import { react2angular } from 'react2angular'; import { react2angular } from 'react2angular';
import { Loader } from './base/loader'; import { Loader } from '../base/loader';
import { Event } from '../models/event'; import { Event } from '../../models/event';
import { EventTheme } from '../models/event-theme'; import { EventTheme } from '../../models/event-theme';
import { IApplication } from '../models/application'; import { IApplication } from '../../models/application';
import EventThemeAPI from '../api/event-theme'; import EventThemeAPI from '../../api/event-theme';
declare const Application: IApplication; 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 * 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 { t } = useTranslation('shared');
const [themes, setThemes] = useState<Array<EventTheme>>([]); const [themes, setThemes] = useState<Array<EventTheme>>([]);
@ -77,10 +77,10 @@ const EventThemes: React.FC<EventThemesProps> = ({ event, onChange }) => {
return ( return (
<div className="event-themes"> <div className="event-themes">
{hasThemes() && <div className="event-themes--panel"> {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"> <div className="content">
<Select defaultValue={defaultValues()} <Select defaultValue={defaultValues()}
placeholder={t('app.shared.event.select_theme')} placeholder={t('app.shared.event_themes.select_theme')}
onChange={handleChange} onChange={handleChange}
options={buildOptions()} options={buildOptions()}
isMulti /> 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/). 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). or from [FormControlledComponent](../models/form-component.ts).
Please look at the existing components for examples. 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 // Compose classnames from props
const classNames = [ const classNames = [
'form-item',
`${className || ''}`, `${className || ''}`,
`${isDirty && fieldError ? 'is-incorrect' : ''}`, `${isDirty && fieldError ? 'is-incorrect' : ''}`,
`${isDirty && warning ? 'is-warned' : ''}`, `${isDirty && warning ? 'is-warned' : ''}`,
@ -59,7 +58,7 @@ export const AbstractFormItem = <TFieldValues extends FieldValues>({ id, label,
} }
return ( return (
<label className={classNames} onClick={handleLabelClick}> <label className={`form-item ${classNames}`} onClick={handleLabelClick}>
{label && <div className='form-item-header'> {label && <div className='form-item-header'>
<p>{label}</p> <p>{label}</p>
{tooltip && <div className="item-tooltip"> {tooltip && <div className="item-tooltip">

View File

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

View File

@ -30,6 +30,9 @@ interface ChangeGroupProps {
*/ */
type selectOption = { value: number, label: string }; 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 }) => { export const ChangeGroup: React.FC<ChangeGroupProps> = ({ user, onSuccess, onError, allowChange, className }) => {
const { t } = useTranslation('shared'); 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 * 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. * 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'); const { t } = useTranslation('public');
// shall we display a loader to prevent double-clicking, while the machine details are loading? // 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); onShowMachine(machine);
}; };
/**
* Return the machine's picture or a placeholder
*/
const machinePicture = (): ReactNode => { const machinePicture = (): ReactNode => {
if (!machine.machine_image) { if (!machine.machine_image) {
return <div className="machine-picture no-picture" />; 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 ( return (
<Loader> <Loader>
<MachineCardComponent user={user} machine={machine} onShowMachine={onShowMachine} onReserveMachine={onReserveMachine} onError={onError} onSuccess={onSuccess} onLoginRequested={onLoginRequested} onEnrollRequested={onEnrollRequested} canProposePacks={canProposePacks} /> <MachineCard {...props} />
</Loader> </Loader>
); );
}; };
export { MachineCardWrapper as MachineCard };

View File

@ -12,6 +12,9 @@ interface MachinesFiltersProps {
*/ */
type selectOption = { value: boolean, label: string }; type selectOption = { value: boolean, label: string };
/**
* Allows filtering on machines list
*/
export const MachinesFilters: React.FC<MachinesFiltersProps> = ({ onStatusSelected }) => { export const MachinesFilters: React.FC<MachinesFiltersProps> = ({ onStatusSelected }) => {
const { t } = useTranslation('public'); 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. * 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 // shown machines
const [machines, setMachines] = useState<Array<Machine>>(null); const [machines, setMachines] = useState<Array<Machine>>(null);
// we keep the full list of machines, for filtering // 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 { useTranslation } from 'react-i18next';
import { HtmlTranslate } from '../base/html-translate'; import { HtmlTranslate } from '../base/html-translate';
import FormatLib from '../../lib/format'; import FormatLib from '../../lib/format';
import { TDateISO } from '../../typings/date-iso';
interface PendingTrainingModalProps { interface PendingTrainingModalProps {
isOpen: boolean, isOpen: boolean,
toggleModal: () => void, 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 * 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) }); 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 => { const header = (): ReactNode => {
return ( return (
<div className="user-info"> <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> <span className="user-name">{user?.name}</span>
</div> </div>
); );

View File

@ -31,7 +31,7 @@ interface ReserveButtonProps {
/** /**
* Button component that makes the training verification before redirecting the user to the reservation calendar * 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 { t } = useTranslation('shared');
const [machine, setMachine] = useState<Machine>(null); 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 ( return (
<Loader> <Loader>
<ReserveButtonComponent currentUser={currentUser} machineId={machineId} onError={onError} onSuccess={onSuccess} onLoadingStart={onLoadingStart} onLoadingEnd={onLoadingEnd} onReserveMachine={onReserveMachine} onLoginRequested={onLoginRequested} onEnrollRequested={onEnrollRequested} className={className} canProposePacks={canProposePacks}> <ReserveButton {...props}>
{children} {props.children}
</ReserveButtonComponent> </ReserveButton>
</Loader> </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 ( return (
<div className="payment-schedule-summary"> <div className="payment-schedule-summary">
<div> <div>
<h4>{ t('app.shared.cart.your_payment_schedule') }</h4> <h4>{ t('app.shared.payment_schedule_summary.your_payment_schedule') }</h4>
{hasEqualDeadlines() && <ul> {hasEqualDeadlines() && <ul>
<li> <li>
<span className="schedule-item-info"> <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>
<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> </li>
</ul>} </ul>}
{!hasEqualDeadlines() && <ul> {!hasEqualDeadlines() && <ul>
<li> <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-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>
<li> <li>
<span className="schedule-item-info"> <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> </span>
</li> </li>
</ul>} </ul>}
<button className="view-full-schedule" onClick={toggleFullScheduleModal}>{t('app.shared.cart.view_full_schedule')}</button> <button className="view-full-schedule" onClick={toggleFullScheduleModal}>{t('app.shared.payment_schedule_summary.view_full_schedule')}</button>
<FabModal title={t('app.shared.cart.your_payment_schedule')} isOpen={modal} toggleModal={toggleFullScheduleModal}> <FabModal title={t('app.shared.payment_schedule_summary.your_payment_schedule')} isOpen={modal} toggleModal={toggleFullScheduleModal}>
<ul className="full-schedule"> <ul className="full-schedule">
{schedule.items.map(item => ( {schedule.items.map(item => (
<li key={String(item.due_date)}> <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 * This component shows a list of all payment schedules with their associated deadlines (aka. PaymentScheduleItem) and invoices
* for the currentUser * for the currentUser
*/ */
const PaymentSchedulesDashboard: React.FC<PaymentSchedulesDashboardProps> = ({ currentUser, onError, onCardUpdateSuccess }) => { export const PaymentSchedulesDashboard: React.FC<PaymentSchedulesDashboardProps> = ({ currentUser, onError, onCardUpdateSuccess }) => {
const { t } = useTranslation('logged'); const { t } = useTranslation('logged');
// list of displayed payment schedules // 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 * after a successful card update, provide a success message to the end-user
*/ */
const handleCardUpdateSuccess = (): void => { 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 ( return (
<div className="payment-schedules-dashboard"> <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"> {hasSchedules() && <div className="schedules-list">
<PaymentSchedulesTable paymentSchedules={paymentSchedules} <PaymentSchedulesTable paymentSchedules={paymentSchedules}
showCustomer={false} showCustomer={false}
@ -93,7 +93,7 @@ const PaymentSchedulesDashboard: React.FC<PaymentSchedulesDashboardProps> = ({ c
operator={currentUser} operator={currentUser}
onError={onError} onError={onError}
onCardUpdateSuccess={handleCardUpdateSuccess} /> 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>}
</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 * 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'); const { t } = useTranslation('admin');
// list of displayed payment schedules // 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 * after a successful card update, provide a success message to the operator
*/ */
const handleCardUpdateSuccess = (): void => { 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 ( return (
<div className="payment-schedules-list"> <div className="payment-schedules-list">
<h3> <h3>
<i className="fas fa-filter" /> <i className="fas fa-filter" />
{t('app.admin.invoices.payment_schedules.filter_schedules')} {t('app.admin.invoices.payment_schedules_list.filter_schedules')}
</h3> </h3>
<div className="schedules-filters"> <div className="schedules-filters">
<DocumentFilters onFilterChange={handleFiltersChange} /> <DocumentFilters onFilterChange={handleFiltersChange} />
</div> </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"> {hasSchedules() && <div className="schedules-list">
<PaymentSchedulesTable paymentSchedules={paymentSchedules} <PaymentSchedulesTable paymentSchedules={paymentSchedules}
showCustomer={true} showCustomer={true}
@ -113,7 +113,7 @@ const PaymentSchedulesList: React.FC<PaymentSchedulesListProps> = ({ currentUser
operator={currentUser} operator={currentUser}
onError={onError} onError={onError}
onCardUpdateSuccess={handleCardUpdateSuccess} /> 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>}
</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 * 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'); const { t } = useTranslation('shared');
// for each payment schedule: are the details (all deadlines) shown or hidden? // 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 => { const expandCollapseIcon = (paymentScheduleId: number): JSX.Element => {
if (isExpanded(paymentScheduleId)) { if (isExpanded(paymentScheduleId)) {
// eslint-disable-next-line fabmanager/component-class-named-as-component
return <i className="fas fa-minus-square" />; return <i className="fas fa-minus-square" />;
} else { } else {
// eslint-disable-next-line fabmanager/component-class-named-as-component
return <i className="fas fa-plus-square" />; return <i className="fas fa-plus-square" />;
} }
}; };
@ -93,9 +95,10 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
const downloadScheduleButton = (id: number): JSX.Element => { const downloadScheduleButton = (id: number): JSX.Element => {
const link = `api/payment_schedules/${id}/download`; const link = `api/payment_schedules/${id}/download`;
return ( return (
// eslint-disable-next-line fabmanager/component-class-named-as-component
<a href={link} target="_blank" className="download-button" rel="noreferrer"> <a href={link} target="_blank" className="download-button" rel="noreferrer">
<i className="fas fa-download" /> <i className="fas fa-download" />
{t('app.shared.schedules_table.download')} {t('app.shared.payment_schedules_table.download')}
</a> </a>
); );
}; };
@ -104,11 +107,12 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
* Return the human-readable string for the status of the provided deadline. * Return the human-readable string for the status of the provided deadline.
*/ */
const formatState = (item: PaymentScheduleItem, schedule: PaymentSchedule): JSX.Element => { 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) { if (item.state === PaymentScheduleItemState.Paid) {
const key = `app.shared.schedules_table.method_${item.payment_method}`; const key = `app.shared.schedules_table.method_${item.payment_method}`;
res += ` (${t(key)})`; res += ` (${t(key)})`;
} }
// eslint-disable-next-line fabmanager/component-class-named-as-component
return <span className={`state-${item.state}`}>{res}</span>; return <span className={`state-${item.state}`}>{res}</span>;
}; };
@ -119,16 +123,19 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
refreshList(); refreshList();
}; };
/**
* Return the JSX table element that list all payment schedules and allows to perform actions on them.
*/
const renderPaymentSchedulesTable = (): ReactElement => { const renderPaymentSchedulesTable = (): ReactElement => {
return ( return (
<table className="schedules-table"> <table className="payment-schedules-table">
<thead> <thead>
<tr> <tr>
<th className="w-35" /> <th className="w-35" />
<th className="w-200">{t('app.shared.schedules_table.schedule_num')}</th> <th className="w-200">{t('app.shared.payment_schedules_table.schedule_num')}</th>
<th className="w-200">{t('app.shared.schedules_table.date')}</th> <th className="w-200">{t('app.shared.payment_schedules_table.date')}</th>
<th className="w-120">{t('app.shared.schedules_table.price')}</th> <th className="w-120">{t('app.shared.payment_schedules_table.price')}</th>
{showCustomer && <th className="w-200">{t('app.shared.schedules_table.customer')}</th>} {showCustomer && <th className="w-200">{t('app.shared.payment_schedules_table.customer')}</th>}
<th className="w-200"/> <th className="w-200"/>
</tr> </tr>
</thead> </thead>
@ -152,9 +159,9 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
<table className="schedule-items-table"> <table className="schedule-items-table">
<thead> <thead>
<tr> <tr>
<th className="w-120">{t('app.shared.schedules_table.deadline')}</th> <th className="w-120">{t('app.shared.payment_schedules_table.deadline')}</th>
<th className="w-120">{t('app.shared.schedules_table.amount')}</th> <th className="w-120">{t('app.shared.payment_schedules_table.amount')}</th>
<th className="w-200">{t('app.shared.schedules_table.state')}</th> <th className="w-200">{t('app.shared.payment_schedules_table.state')}</th>
<th className="w-200" /> <th className="w-200" />
</tr> </tr>
</thead> </thead>
@ -212,12 +219,14 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
return <div />; 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 ( return (
<Loader> <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> </Loader>
); );
}; };
export { PaymentSchedulesTableWrapper as PaymentSchedulesTable };

View File

@ -25,7 +25,7 @@ export const SelectSchedule: React.FC<SelectScheduleProps> = ({ show, selected,
return ( return (
<div className="select-schedule"> <div className="select-schedule">
{show && <div className={className || ''}> {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" /> <Switch checked={selected} id="payment_schedule" onChange={onChange} className="schedule-switch" />
</div>} </div>}
</div> </div>

View File

@ -19,6 +19,9 @@ interface UpdatePaymentMeanModalProps {
*/ */
type selectOption = { value: PaymentMethod, label: string }; 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 }) => { export const UpdatePaymentMeanModal: React.FC<UpdatePaymentMeanModalProps> = ({ isOpen, toggleModal, onError, afterSuccess, paymentSchedule }) => {
const { t } = useTranslation('admin'); const { t } = useTranslation('admin');

View File

@ -1,7 +1,7 @@
import React, { FunctionComponent, ReactNode, useEffect, useRef, useState } from 'react'; import React, { FunctionComponent, ReactNode, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import WalletLib from '../../lib/wallet'; import WalletLib from '../../lib/wallet';
import { WalletInfo } from '../wallet-info'; import { WalletInfo } from './wallet-info';
import { FabModal, ModalSize } from '../base/fab-modal'; import { FabModal, ModalSize } from '../base/fab-modal';
import { HtmlTranslate } from '../base/html-translate'; import { HtmlTranslate } from '../base/html-translate';
import { CustomAsset, CustomAssetName } from '../../models/custom-asset'; import { CustomAsset, CustomAssetName } from '../../models/custom-asset';
@ -220,13 +220,13 @@ export const AbstractPaymentModal: React.FC<AbstractPaymentModalProps> = ({ isOp
{errors} {errors}
</div>} </div>}
{hasPaymentScheduleInfo() && <div className="payment-schedule-info"> {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>} </div>}
{hasCgv() && <div className="terms-of-sales"> {hasCgv() && <div className="terms-of-sales">
<input type="checkbox" id="acceptToS" name="acceptCondition" checked={tos} onChange={toggleTos} required /> <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"> <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> </a>
</label> </label>
</div>} </div>}
@ -235,8 +235,8 @@ export const AbstractPaymentModal: React.FC<AbstractPaymentModalProps> = ({ isOp
disabled={!canSubmit()} disabled={!canSubmit()}
form={formId} form={formId}
className="validate-btn"> className="validate-btn">
{remainingPrice > 0 && t('app.shared.payment.confirm_payment_of_', { AMOUNT: FormatLib.price(remainingPrice) })} {remainingPrice > 0 && t('app.shared.abstract_payment_modal.confirm_payment_of_', { AMOUNT: FormatLib.price(remainingPrice) })}
{remainingPrice === 0 && t('app.shared.payment.validate')} {remainingPrice === 0 && t('app.shared.abstract_payment_modal.validate')}
</button>} </button>}
{submitState && <div className="payment-pending"> {submitState && <div className="payment-pending">
<div className="fa-2x"> <div className="fa-2x">

View File

@ -2,7 +2,7 @@ import React, { ReactElement, useEffect, useState } from 'react';
import { react2angular } from 'react2angular'; import { react2angular } from 'react2angular';
import { Loader } from '../base/loader'; import { Loader } from '../base/loader';
import { StripeModal } from './stripe/stripe-modal'; import { StripeModal } from './stripe/stripe-modal';
import { PayZenModal } from './payzen/payzen-modal'; import { PayzenModal } from './payzen/payzen-modal';
import { IApplication } from '../../models/application'; import { IApplication } from '../../models/application';
import { ShoppingCart } from '../../models/payment'; import { ShoppingCart } from '../../models/payment';
import { User } from '../../models/user'; 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 * This component open a modal dialog for the configured payment gateway, allowing the user to input his card data
* to process an online payment. * 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 { t } = useTranslation('shared');
const [gateway, setGateway] = useState<Setting>(null); const [gateway, setGateway] = useState<Setting>(null);
@ -58,7 +58,7 @@ const CardPaymentModalComponent: React.FC<CardPaymentModalProps> = ({ isOpen, to
* Render the PayZen payment modal * Render the PayZen payment modal
*/ */
const renderPayZenModal = (): ReactElement => { const renderPayZenModal = (): ReactElement => {
return <PayZenModal isOpen={isOpen} return <PayzenModal isOpen={isOpen}
toggleModal={toggleModal} toggleModal={toggleModal}
afterSuccess={afterSuccess} afterSuccess={afterSuccess}
onError={onError} onError={onError}
@ -80,21 +80,23 @@ const CardPaymentModalComponent: React.FC<CardPaymentModalProps> = ({ isOpen, to
return renderPayZenModal(); return renderPayZenModal();
case null: case null:
case undefined: case undefined:
onError(t('app.shared.payment_modal.online_payment_disabled')); onError(t('app.shared.card_payment_modal.online_payment_disabled'));
return <div />; return <div />;
default: 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}`); console.error(`[PaymentModal] Unimplemented gateway: ${gateway.value}`);
return <div />; return <div />;
} }
}; };
export const CardPaymentModal: React.FC<CardPaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, currentUser, schedule, cart, customer }) => { const CardPaymentModalWrapper: React.FC<CardPaymentModalProps> = (props) => {
return ( return (
<Loader> <Loader>
<CardPaymentModalComponent isOpen={isOpen} toggleModal={toggleModal} afterSuccess={afterSuccess} onError={onError} currentUser={currentUser} schedule={schedule} cart={cart} customer={customer} /> <CardPaymentModal {...props} />
</Loader> </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 => { const methodToOption = (value: scheduleMethod): selectOption => {
if (!value) return { value, label: '' }; 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 { try {
const online = await SettingAPI.get(SettingName.OnlinePaymentModule); const online = await SettingAPI.get(SettingName.OnlinePaymentModule);
if (online.value !== 'true') { 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(); return toggleOnlinePaymentModal();
} catch (e) { } catch (e) {
@ -118,21 +118,21 @@ export const LocalPaymentForm: React.FC<GatewayFormProps> = ({ onSubmit, onSucce
}; };
return ( return (
<form onSubmit={handleSubmit} id={formId} className={className || ''}> <form onSubmit={handleSubmit} id={formId} className={`local-payment-form ${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_form.about_to_cash')}</p>}
{!paymentSchedule && isFreeOfCharge() && <p className="payment">{t('app.admin.local_payment.about_to_confirm', { ITEM: mainItemType() })}</p>} {!paymentSchedule && isFreeOfCharge() && <p className="payment">{t('app.admin.local_payment_form.about_to_confirm', { ITEM: mainItemType() })}</p>}
{paymentSchedule && <div className="payment-schedule"> {paymentSchedule && <div className="payment-schedule">
<div className="schedule-method"> <div className="schedule-method">
<label htmlFor="payment-method">{t('app.admin.local_payment.payment_method')}</label> <label htmlFor="payment-method">{t('app.admin.local_payment_form.payment_method')}</label>
<Select placeholder={ t('app.admin.local_payment.payment_method') } <Select placeholder={ t('app.admin.local_payment_form.payment_method') }
id="payment-method" id="payment-method"
className="method-select" className="method-select"
onChange={handleUpdateMethod} onChange={handleUpdateMethod}
options={buildMethodOptions()} options={buildMethodOptions()}
value={methodToOption(method)} /> value={methodToOption(method)} />
{method === 'card' && <p>{t('app.admin.local_payment.card_collection_info')}</p>} {method === 'card' && <p>{t('app.admin.local_payment_form.card_collection_info')}</p>}
{method === 'check' && <p>{t('app.admin.local_payment.check_collection_info', { DEADLINES: paymentSchedule.items.length })}</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.transfer_collection_info" options={{ DEADLINES: paymentSchedule.items.length }} />} {method === 'transfer' && <HtmlTranslate trKey="app.admin.local_payment_form.transfer_collection_info" options={{ DEADLINES: paymentSchedule.items.length }} />}
</div> </div>
<div className="full-schedule"> <div className="full-schedule">
<ul> <ul>

View File

@ -28,7 +28,7 @@ interface LocalPaymentModalProps {
/** /**
* This component enables a privileged user to confirm a local payments. * 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'); const { t } = useTranslation('admin');
/** /**
@ -76,7 +76,7 @@ const LocalPaymentModalComponent: React.FC<LocalPaymentModalProps> = ({ isOpen,
isOpen={isOpen} isOpen={isOpen}
toggleModal={toggleModal} toggleModal={toggleModal}
logoFooter={logoFooter()} 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" formId="local-payment-form"
formClassName="local-payment-form" formClassName="local-payment-form"
currentUser={currentUser} 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 ( return (
<Loader> <Loader>
<LocalPaymentModalComponent isOpen={isOpen} toggleModal={toggleModal} afterSuccess={afterSuccess} onError={onError} currentUser={currentUser} schedule={schedule} cart={cart} updateCart={updateCart} customer={customer} /> <LocalPaymentModal {...props} />
</Loader> </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 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 }) => { export const PayzenCardUpdateModal: React.FC<PayzenCardUpdateModalProps> = ({ isOpen, toggleModal, onSuccess, schedule, operator }) => {
const { t } = useTranslation('shared'); const { t } = useTranslation('shared');
@ -61,7 +64,7 @@ export const PayzenCardUpdateModal: React.FC<PayzenCardUpdateModalProps> = ({ is
toggleModal={toggleModal} toggleModal={toggleModal}
closeButton={false} closeButton={false}
customFooter={logoFooter()} customFooter={logoFooter()}
className="payzen-update-card-modal"> className="payzen-card-update-modal">
{schedule && <PayzenForm onSubmit={handleCardUpdateSubmit} {schedule && <PayzenForm onSubmit={handleCardUpdateSubmit}
onSuccess={onSuccess} onSuccess={onSuccess}
onError={handleCardUpdateError} onError={handleCardUpdateError}

View File

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

View File

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

View File

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

View File

@ -83,7 +83,7 @@ export const PayzenSettings: React.FC<PayzenSettingsProps> = ({ onEditKeys, onCu
setError(''); setError('');
updateSettings(draft => draft.set(SettingName.PayZenCurrency, value)); updateSettings(draft => draft.set(SettingName.PayZenCurrency, value));
} else { } 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)); updateSettings(draft => draft.set(SettingName.PayZenCurrency, result.value));
onCurrencyUpdateSuccess(result.value); onCurrencyUpdateSuccess(result.value);
}, reason => { }, 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 ( return (
<div className="payzen-settings"> <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"> <div className="payzen-keys">
{payZenPublicSettings.concat(payZenPrivateSettings).map(setting => { {payZenPublicSettings.concat(payZenPrivateSettings).map(setting => {
return ( return (
<div className="key-wrapper" key={setting}> <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)} <FabInput defaultValue={settings.get(setting)}
id={setting} id={setting}
type={payZenPrivateSettings.indexOf(setting) > -1 ? 'password' : 'text'} type={payZenPrivateSettings.indexOf(setting) > -1 ? 'password' : 'text'}
@ -119,17 +119,17 @@ export const PayzenSettings: React.FC<PayzenSettingsProps> = ({ onEditKeys, onCu
); );
})} })}
<div className="edit-keys"> <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> </div>
<div className="payzen-currency"> <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"> <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> </p>
<div className="payzen-currency-form"> <div className="payzen-currency-form">
<div className="currency-wrapper"> <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)} <FabInput defaultValue={settings.get(SettingName.PayZenCurrency)}
id="payzen_currency" id="payzen_currency"
icon={<i className="fas fa-money-bill" />} icon={<i className="fas fa-money-bill" />}
@ -138,7 +138,7 @@ export const PayzenSettings: React.FC<PayzenSettingsProps> = ({ onEditKeys, onCu
pattern="[A-Z]{3}" pattern="[A-Z]{3}"
error={error} /> error={error} />
</div> </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> </div>
</div> </div>

View File

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

View File

@ -16,6 +16,9 @@ interface StripeCardUpdateModalProps {
operator: User 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 }) => { export const StripeCardUpdateModal: React.FC<StripeCardUpdateModalProps> = ({ isOpen, toggleModal, onSuccess, schedule, operator }) => {
const { t } = useTranslation('shared'); const { t } = useTranslation('shared');
@ -30,7 +33,7 @@ export const StripeCardUpdateModal: React.FC<StripeCardUpdateModalProps> = ({ is
const logoFooter = (): ReactNode => { const logoFooter = (): ReactNode => {
return ( return (
<div className="stripe-modal-icons"> <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={stripeLogo} alt="powered by stripe" />
<img src={mastercardLogo} alt="mastercard" /> <img src={mastercardLogo} alt="mastercard" />
<img src={visaLogo} alt="visa" /> <img src={visaLogo} alt="visa" />
@ -59,7 +62,7 @@ export const StripeCardUpdateModal: React.FC<StripeCardUpdateModalProps> = ({ is
toggleModal={toggleModal} toggleModal={toggleModal}
closeButton={false} closeButton={false}
customFooter={logoFooter()} customFooter={logoFooter()}
className="stripe-update-card-modal"> className="stripe-card-update-modal">
{schedule && <StripeCardUpdate onSubmit={handleCardUpdateSubmit} {schedule && <StripeCardUpdate onSubmit={handleCardUpdateSubmit}
onSuccess={onSuccess} onSuccess={onSuccess}
onError={handleCardUpdateError} onError={handleCardUpdateError}

View File

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

View File

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

View File

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

View File

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

View File

@ -36,7 +36,7 @@ export const StripeModal: React.FC<StripeModalProps> = ({ isOpen, toggleModal, a
const logoFooter = (): ReactNode => { const logoFooter = (): ReactNode => {
return ( return (
<div className="stripe-modal-icons"> <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={stripeLogo} alt="powered by stripe" />
<img src={mastercardLogo} alt="mastercard" /> <img src={mastercardLogo} alt="mastercard" />
<img src={visaLogo} alt="visa" /> <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 * This component open a modal dialog for the configured payment gateway, allowing the user to input his card data
* to process an online payment. * 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 { t } = useTranslation('shared');
const [gateway, setGateway] = useState<string>(''); 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 ( return (
<Loader> <Loader>
<UpdateCardModalComponent isOpen={isOpen} toggleModal={toggleModal} afterSuccess={afterSuccess} onError={onError} operator={operator} schedule={schedule} /> <UpdateCardModal {...props} />
</Loader> </Loader>
); );
}; };
export { UpdateCardModalWrapper as UpdateCardModal };

View File

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

View File

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

View File

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

View File

@ -16,7 +16,10 @@ interface PlanCategoryFormProps {
onError: (message: string) => void 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 { t } = useTranslation('admin');
const { register, control, handleSubmit } = useForm<PlanCategory>({ defaultValues: { ...category } }); const { register, control, handleSubmit } = useForm<PlanCategory>({ defaultValues: { ...category } });
@ -28,16 +31,16 @@ const PlanCategoryFormComponent: React.FC<PlanCategoryFormProps> = ({ action, ca
switch (action) { switch (action) {
case 'create': case 'create':
PlanCategoryAPI.create(data).then(() => { 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) => { }).catch((error) => {
onError(t('app.admin.manage_plan_category.create_category.error') + error); onError(t('app.admin.plan_category_form.create.error') + error);
}); });
break; break;
case 'update': case 'update':
PlanCategoryAPI.update(data).then(() => { 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) => { }).catch((error) => {
onError(t('app.admin.manage_plan_category.update_category.error') + error); onError(t('app.admin.plan_category_form.update.error') + error);
}); });
break; break;
} }
@ -45,23 +48,25 @@ const PlanCategoryFormComponent: React.FC<PlanCategoryFormProps> = ({ action, ca
return ( return (
<form onSubmit={handleSubmit(onSubmit)}> <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"> <FabAlert level="info" className="significance-info">
{t('app.admin.manage_plan_category.info')} {t('app.admin.plan_category_form.info')}
</FabAlert> </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> </form>
); );
}; };
export const PlanCategoryForm: React.FC<PlanCategoryFormProps> = ({ action, category, onSuccess, onError }) => { const PlanCategoryFormWrapper: React.FC<PlanCategoryFormProps> = (props) => {
return ( return (
<Loader> <Loader>
<PlanCategoryFormComponent action={action} category={category} onSuccess={onSuccess} onError={onError} /> <PlanCategoryForm {...props} />
</Loader> </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. * 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'); const { t } = useTranslation('public');
/** /**
* Return the formatted localized amount of the given plan (eg. 20.5 => "20,50 €") * 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"> <div className="content">
{canBeScheduled() && <div className="wrap-monthly"> {canBeScheduled() && <div className="wrap-monthly">
<div className="price"> <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> <span className="period">{duration()}</span>
</div> </div>
</div>} </div>}
@ -118,25 +118,25 @@ const PlanCardComponent: React.FC<PlanCardProps> = ({ plan, userId, subscribedPl
</div> </div>
<div className="card-footer"> <div className="card-footer">
{hasDescription() && <div className="plan-description" dangerouslySetInnerHTML={{ __html: plan.description }}/>} {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"> {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>} </div>}
{canSubscribeForMe() && <div className="cta-button"> {canSubscribeForMe() && <div className="cta-button">
{!hasSubscribedToThisPlan() && <button className={`subscribe-button ${isSelected ? 'selected-card' : ''}`} {!hasSubscribedToThisPlan() && <button className={`subscribe-button ${isSelected ? 'selected-card' : ''}`}
onClick={handleSelectPlan} onClick={handleSelectPlan}
disabled={!_.isNil(subscribedPlanId)}> disabled={!_.isNil(subscribedPlanId)}>
{t('app.public.plans.i_choose_that_plan')} {t('app.public.plan_card.i_choose_that_plan')}
</button>} </button>}
{hasSubscribedToThisPlan() && <button className="subscribe-button selected-card" disabled> {hasSubscribedToThisPlan() && <button className="subscribe-button selected-card" disabled>
{ t('app.public.plans.i_already_subscribed') } { t('app.public.plan_card.i_already_subscribed') }
</button>} </button>}
</div>} </div>}
{canSubscribeForOther() && <div className="cta-button"> {canSubscribeForOther() && <div className="cta-button">
<button className={`subscribe-button ${isSelected ? 'selected-card' : ''}`} <button className={`subscribe-button ${isSelected ? 'selected-card' : ''}`}
onClick={handleSelectPlan} onClick={handleSelectPlan}
disabled={_.isNil(userId)}> 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> </button>
</div>} </div>}
</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 ( return (
<Loader> <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> </Loader>
); );
}; };
export { PlanCardWrapper as PlanCard };

View File

@ -20,6 +20,9 @@ interface PlansFilterProps {
*/ */
type selectOption = { value: number, label: string }; type selectOption = { value: number, label: string };
/**
* Allows filtering on plans list
*/
export const PlansFilter: React.FC<PlansFilterProps> = ({ user, groups, onGroupSelected, onError, onDurationSelected }) => { export const PlansFilter: React.FC<PlansFilterProps> = ({ user, groups, onGroupSelected, onError, onDurationSelected }) => {
const { t } = useTranslation('public'); 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 * 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 // all plans
const [plans, setPlans] = useState<PlansTree>(null); const [plans, setPlans] = useState<PlansTree>(null);
// all plan-categories, ordered by weight // all plan-categories, ordered by weight

View File

@ -14,6 +14,7 @@ import { react2angular } from 'react2angular';
import { IApplication } from '../../models/application'; import { IApplication } from '../../models/application';
import { PrepaidPack } from '../../models/prepaid-pack'; import { PrepaidPack } from '../../models/prepaid-pack';
import PrepaidPackAPI from '../../api/prepaid-pack'; import PrepaidPackAPI from '../../api/prepaid-pack';
import { FabAlert } from '../base/fab-alert';
declare const Application: IApplication; declare const Application: IApplication;
@ -29,7 +30,11 @@ interface PacksSummaryProps {
refresh?: Promise<void> 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 { t } = useTranslation('logged');
const [packs, setPacks] = useState<Array<PrepaidPack>>(null); const [packs, setPacks] = useState<Array<PrepaidPack>>(null);
@ -140,9 +145,9 @@ const PacksSummaryComponent: React.FC<PacksSummaryProps> = ({ item, itemType, cu
<span className="remaining-hours"> <span className="remaining-hours">
{t('app.logged.packs_summary.remaining_HOURS', { HOURS: totalHours(), ITEM: itemType })} {t('app.logged.packs_summary.remaining_HOURS', { HOURS: totalHours(), ITEM: itemType })}
{isPackOnlyForSubscription && !customer.subscribed_plan && {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')} {t('app.logged.packs_summary.unable_to_use_pack_for_subsription_is_expired')}
</div> </FabAlert>
} }
</span> </span>
</div> </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 ( return (
<Loader> <Loader>
<PacksSummaryComponent item={item} itemType={itemType} customer={customer} operator={operator} onError={onError} onSuccess={onSuccess} refresh={refresh} /> <PacksSummary {...props} />
</Loader> </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(); toggleEdit();
}; };
/**
* Callback triggered when the user input a new price
*/
const handleChangePrice = (value: string): void => {
setTempPrice(value);
};
/** /**
* Enable or disable the edit mode * Enable or disable the edit mode
*/ */
@ -41,7 +48,7 @@ export const EditablePrice: React.FC<EditablePriceProps> = ({ price, onSave }) =
<span className="editable-price"> <span className="editable-price">
{!edit && <span className="display-price" onClick={toggleEdit}>{FormatLib.price(price.amount)}</span>} {!edit && <span className="display-price" onClick={toggleEdit}>{FormatLib.price(price.amount)}</span>}
{edit && <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-check" />} className="approve-button" onClick={handleValidateEdit} />
<FabButton icon={<i className="fas fa-times" />} className="cancel-button" onClick={toggleEdit} /> <FabButton icon={<i className="fas fa-times" />} className="cancel-button" onClick={toggleEdit} />
</span>} </span>}

View File

@ -16,7 +16,7 @@ interface DeletePackProps {
* This component shows a button. * 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. * 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 { t } = useTranslation('admin');
const [deletionModal, setDeletionModal] = useState<boolean>(false); 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 ( return (
<Loader> <Loader>
<DeletePackComponent onSuccess={onSuccess} onError={onError} pack={pack} /> <DeletePack {...props} />
</Loader> </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 * 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 { t } = useTranslation('admin');
const [machines, setMachines] = useState<Array<Machine>>(null); 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 { EditExtendedPrice } from './edit-extended-price';
import { DeleteExtendedPrice } from './delete-extended-price'; import { DeleteExtendedPrice } from './delete-extended-price';
interface ConfigureExtendedPriceButtonProps { interface ConfigureExtendedPricesButtonProps {
prices: Array<Price>, prices: Array<Price>,
onError: (message: string) => void, onError: (message: string) => void,
onSuccess: (message: string) => void, onSuccess: (message: string) => void,
@ -21,7 +21,7 @@ interface ConfigureExtendedPriceButtonProps {
* This component is a button that shows the list of extendedPrices. * This component is a button that shows the list of extendedPrices.
* It also triggers modal dialogs to configure (add/edit/remove) 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 { t } = useTranslation('admin');
const [extendedPrices, setExtendedPrices] = useState<Array<Price>>(prices); 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}. * The form validation must be created elsewhere, using the attribute form={formId}.
*/ */
export const ExtendedPriceForm: React.FC<ExtendedPriceFormProps> = ({ formId, onSubmit, price }) => { 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 { IApplication } from '../../../models/application';
import { Space } from '../../../models/space'; import { Space } from '../../../models/space';
import { EditablePrice } from '../editable-price'; 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 PriceAPI from '../../../api/price';
import { Price } from '../../../models/price'; import { Price } from '../../../models/price';
import { useImmer } from 'use-immer'; import { useImmer } from 'use-immer';
@ -26,7 +26,7 @@ interface SpacesPricingProps {
/** /**
* Interface to set and edit the prices of spaces-hours, per group * 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 { t } = useTranslation('admin');
const [spaces, setSpaces] = useState<Array<Space>>(null); const [spaces, setSpaces] = useState<Array<Space>>(null);
@ -120,7 +120,7 @@ const SpacesPricing: React.FC<SpacesPricingProps> = ({ onError, onSuccess }) =>
<td>{space.name}</td> <td>{space.name}</td>
{groups?.map(group => <td key={group.id}> {groups?.map(group => <td key={group.id}>
{prices.length && <EditablePrice price={findPriceBy(space.id, group.id)} onSave={handleUpdatePrice} />} {prices.length && <EditablePrice price={findPriceBy(space.id, group.id)} onSave={handleUpdatePrice} />}
<ConfigureExtendedPriceButton <ConfigureExtendedPricesButton
prices={findExtendedPricesBy(space.id, group.id)} prices={findExtendedPricesBy(space.id, group.id)}
onError={onError} onError={onError}
onSuccess={onSuccess} onSuccess={onSuccess}

View File

@ -20,6 +20,13 @@ interface ProfileFormOptionProps {
onSuccess: (user: User) => void, 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 }) => { export const ProfileFormOption: React.FC<ProfileFormOptionProps> = ({ user, activeProvider, onError, onSuccess }) => {
const { t } = useTranslation('logged'); const { t } = useTranslation('logged');

View File

@ -7,6 +7,7 @@ import { Loader } from '../base/loader';
import { IApplication } from '../../models/application'; import { IApplication } from '../../models/application';
import { ProfileCustomField } from '../../models/profile-custom-field'; import { ProfileCustomField } from '../../models/profile-custom-field';
import ProfileCustomFieldAPI from '../../api/profile-custom-field'; import ProfileCustomFieldAPI from '../../api/profile-custom-field';
import { FabButton } from '../base/fab-button';
declare const Application: IApplication; declare const Application: IApplication;
@ -18,7 +19,7 @@ interface ProfileCustomFieldsListProps {
/** /**
* This component shows a list of all profile custom fields * 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 { t } = useTranslation('admin');
const [profileCustomFields, setProfileCustomFields] = useState<Array<ProfileCustomField>>([]); 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) => { const saveProfileCustomField = (profileCustomField: ProfileCustomField) => {
ProfileCustomFieldAPI.update(profileCustomField).then(data => { ProfileCustomFieldAPI.update(profileCustomField).then(data => {
const newFields = profileCustomFields.map(f => { const newFields = profileCustomFields.map(f => {
@ -43,9 +47,9 @@ const ProfileCustomFieldsList: React.FC<ProfileCustomFieldsListProps> = ({ onSuc
if (profileCustomFieldToEdit) { if (profileCustomFieldToEdit) {
setProfileCustomFieldToEdit(null); 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 => { }).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) => { const editProfileCustomFieldLabel = (profileCustomField: ProfileCustomField) => {
return () => { return () => {
setProfileCustomFieldToEdit(_.clone(profileCustomField)); setProfileCustomFieldToEdit(_.clone(profileCustomField));
}; };
}; };
/**
* Callback triggered when the input "label" is changed: updates the according state
*/
const onChangeProfileCustomFieldLabel = (e: BaseSyntheticEvent) => { const onChangeProfileCustomFieldLabel = (e: BaseSyntheticEvent) => {
const { value } = e.target; const { value } = e.target;
setProfileCustomFieldToEdit({ setProfileCustomFieldToEdit({
@ -77,16 +88,22 @@ const ProfileCustomFieldsList: React.FC<ProfileCustomFieldsListProps> = ({ onSuc
}); });
}; };
/**
* Save the currently edited custom field
*/
const saveProfileCustomFieldLabel = () => { const saveProfileCustomFieldLabel = () => {
saveProfileCustomField(profileCustomFieldToEdit); saveProfileCustomField(profileCustomFieldToEdit);
}; };
/**
* Closes the edition form for the currently edited custom field
*/
const cancelEditProfileCustomFieldLabel = () => { const cancelEditProfileCustomFieldLabel = () => {
setProfileCustomFieldToEdit(null); setProfileCustomFieldToEdit(null);
}; };
return ( return (
<table className="table profile-custom-fields-list"> <table className="profile-custom-fields-list">
<thead> <thead>
<tr> <tr>
<th style={{ width: '50%' }}></th> <th style={{ width: '50%' }}></th>
@ -101,31 +118,44 @@ const ProfileCustomFieldsList: React.FC<ProfileCustomFieldsListProps> = ({ onSuc
<td> <td>
{profileCustomFieldToEdit?.id !== field.id && field.label} {profileCustomFieldToEdit?.id !== field.id && field.label}
{profileCustomFieldToEdit?.id !== field.id && ( {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> <i className="fa fa-edit"></i>
</button> </FabButton>
)} )}
{profileCustomFieldToEdit?.id === field.id && ( {profileCustomFieldToEdit?.id === field.id && (
<div> <div>
<input className="profile-custom-field-label-input" style={{ width: '80%', height: '38px' }} type="text" value={profileCustomFieldToEdit.label} onChange={onChangeProfileCustomFieldLabel} /> <input className="edit-field-label-input"
<span className="buttons pull-right"> type="text" value={profileCustomFieldToEdit.label}
<button className="btn btn-success save-profile-custom-field-label m-r-xs" onClick={saveProfileCustomFieldLabel}> onChange={onChangeProfileCustomFieldLabel} />
<span className="buttons">
<FabButton className="save-field-label" onClick={saveProfileCustomFieldLabel}>
<i className="fa fa-check"></i> <i className="fa fa-check"></i>
</button> </FabButton>
<button className="btn btn-default delete-profile-custom-field-label m-r-xs" onClick={cancelEditProfileCustomFieldLabel}> <FabButton className="cancel-field-edition" onClick={cancelEditProfileCustomFieldLabel}>
<i className="fa fa-ban"></i> <i className="fa fa-ban"></i>
</button> </FabButton>
</span> </span>
</div> </div>
)} )}
</td> </td>
<td> <td className="activated">
<label htmlFor="profile-custom-field-actived" className="control-label m-r">{t('app.admin.settings.compte.organization_profile_custom_field.actived')}</label> <label htmlFor="profile-custom-field-actived">
<Switch checked={field.actived} id="profile-custom-field-actived" onChange={handleSwitchChanged(field, 'actived')} className="v-middle"></Switch> {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>
<td> <td className="required">
<label htmlFor="profile-custom-field-required" className="control-label m-r">{t('app.admin.settings.compte.organization_profile_custom_field.required')}</label> <label htmlFor="profile-custom-field-required">
<Switch checked={field.required} disabled={!field.actived} id="profile-custom-field-required" onChange={handleSwitchChanged(field, 'required')} className="v-middle"></Switch> {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> </td>
</tr> </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 = () => { const updateSetting = () => {
SettingAPI.update(name, value ? 'true' : 'false') 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 => { .catch(err => {
if (err.status === 304) return; if (err.status === 304) return;
if (err.status === 423) { 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; return;
} }
console.log(err); 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 ( return (
<div className={`form-group ${className || ''}`}> <div className={`boolean-setting ${className || ''}`}>
<label htmlFor={`setting-${name}`} className="control-label m-r">{label}</label> <label htmlFor={`setting-${name}`}>{label}</label>
<Switch checked={value} id={`setting-${name}}`} onChange={handleChanged} className="v-middle"></Switch> <Switch checked={value} id={`setting-${name}}`} onChange={handleChanged} className="switch"></Switch>
{!hideSave && <FabButton className="btn btn-warning m-l" onClick={handleSave}>{t('app.admin.check_list_setting.save')}</FabButton> } {!hideSave && <FabButton className="save-btn" onClick={handleSave}>{t('app.admin.boolean_setting.save')}</FabButton> }
</div> </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 ( return (
<Loader> <Loader>
<BooleanSetting label={label} name={name} onError={onError} onSuccess={onSuccess} onChange={onChange} className={className} hideSave={hideSave} onBeforeSave={onBeforeSave} /> <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 = () => { const handleSave = () => {
SettingAPI.update(name, value) 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)); .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 ( return (
<Loader> <Loader>
<CheckListSetting availableOptions={availableOptions} label={label} name={name} onError={onError} onSuccess={onSuccess} className={className} hideSave={hideSave} defaultValue={defaultValue} onChange={onChange} /> <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 { FabButton } from '../base/fab-button';
import { BooleanSetting } from './boolean-setting'; import { BooleanSetting } from './boolean-setting';
import { CheckListSetting } from './check-list-setting'; import { CheckListSetting } from './check-list-setting';
import { FabAlert } from '../base/fab-alert';
declare const Application: IApplication; 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 { t } = useTranslation('admin');
const [userValidationRequired, setUserValidationRequired] = useState<string>('false'); const [userValidationRequired, setUserValidationRequired] = useState<string>('false');
const userValidationRequiredListDefault = ['subscription', 'machine', 'event', 'space', 'training', 'pack']; const userValidationRequiredListDefault = ['subscription', 'machine', 'event', 'space', 'training', 'pack'];
const [userValidationRequiredList, setUserValidationRequiredList] = useState<string>(null); const [userValidationRequiredList, setUserValidationRequiredList] = useState<string>(null);
const userValidationRequiredOptions = userValidationRequiredListDefault.map(l => { 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) SettingAPI.update(name, value)
.then(() => { .then(() => {
if (name === SettingName.UserValidationRequired) { 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 => { }).catch(err => {
if (err.status === 304) return; if (err.status === 304) return;
if (err.status === 423) { if (err.status === 423) {
if (name === SettingName.UserValidationRequired) { 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; return;
} }
console.log(err); 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 ( return (
<div className="user-validation-setting"> <div className="user-validation-setting">
<BooleanSetting name={SettingName.UserValidationRequired} <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} hideSave={true}
onChange={setUserValidationRequired} onChange={setUserValidationRequired}
onSuccess={onSuccess} onSuccess={onSuccess}
@ -78,13 +83,13 @@ const UserValidationSetting: React.FC<UserValidationSettingProps> = ({ onSuccess
</BooleanSetting> </BooleanSetting>
{userValidationRequired === 'true' && {userValidationRequired === 'true' &&
<div> <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> <p>
{t('app.admin.settings.compte.user_validation_required_list_info')} {t('app.admin.settings.account.user_validation_setting.user_validation_required_list_info')}
</p>
<p className="alert alert-warning">
{t('app.admin.settings.compte.user_validation_required_list_other_info')}
</p> </p>
<FabAlert level="warning">
{t('app.admin.settings.account.user_validation_setting.user_validation_required_list_other_info')}
</FabAlert>
<CheckListSetting name={SettingName.UserValidationRequiredList} <CheckListSetting name={SettingName.UserValidationRequiredList}
label="" label=""
availableOptions={userValidationRequiredOptions} availableOptions={userValidationRequiredOptions}
@ -96,7 +101,7 @@ const UserValidationSetting: React.FC<UserValidationSettingProps> = ({ onSuccess
</CheckListSetting> </CheckListSetting>
</div> </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> </div>
); );
}; };

View File

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

View File

@ -20,6 +20,9 @@ interface FabSocialsProps {
onSuccess: (message: string) => void 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 }) => { export const FabSocials: React.FC<FabSocialsProps> = ({ show = false, onError, onSuccess }) => {
const { t } = useTranslation('shared'); const { t } = useTranslation('shared');
// regular expression to validate the the input fields // 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 !== '')); setSelectedNetworks(fabNetworks.filter(el => el.url !== ''));
}, [fabNetworks]); }, [fabNetworks]);
/**
* Callback triggered when the social networks are saved
*/
const onSubmit = (data) => { const onSubmit = (data) => {
const updatedNetworks = new Map<SettingName, string>(); const updatedNetworks = new Map<SettingName, string>();
Object.keys(data).forEach(key => updatedNetworks.set(key as SettingName, data[key])); 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) => { const selectNetwork = (network) => {
setSelectedNetworks([...selectedNetworks, 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) => { const remove = (network) => {
setSelectedNetworks(selectedNetworks.filter(el => el !== network)); setSelectedNetworks(selectedNetworks.filter(el => el !== network));
setValue(network.name, ''); setValue(network.name, '');
}; };
return ( return (
<>{show <div className="fab-socials">{show
? (selectedNetworks.length > 0) && <> ? (selectedNetworks.length > 0) && <>
<h2>{t('app.shared.fab_socials.follow_us')}</h2> <h2>{t('app.shared.fab_socials.follow_us')}</h2>
<div className='social-icons'> <div className='social-icons'>
@ -94,7 +107,7 @@ export const FabSocials: React.FC<FabSocialsProps> = ({ show = false, onError, o
rules={{ rules={{
pattern: { pattern: {
value: urlRegex, value: urlRegex,
message: t('app.shared.user_profile_form.website_invalid') message: t('app.shared.fab_socials.website_invalid')
} }
}} }}
formState={formState} formState={formState}
@ -107,11 +120,11 @@ export const FabSocials: React.FC<FabSocialsProps> = ({ show = false, onError, o
)} )}
</div>} </div>}
<FabButton type='submit' <FabButton type='submit'
className='btn-warning'> className='save-btn'>
{t('app.shared.buttons.save')} {t('app.shared.fab_socials.save')}
</FabButton> </FabButton>
</form> </form>
}</> }</div>
); );
}; };

View File

@ -26,7 +26,7 @@ interface FreeExtendModalProps {
/** /**
* Modal dialog shown to extend the current subscription of a customer, for free * 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 // we do not render the modal if the subscription was not provided
if (!subscription) return null; 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 * 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 // we do not render the modal if the subscription was not provided
if (!subscription) return null; 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 * Return the formatted localized date for the given date
*/ */
const formatDateTime = (date: Date|TDateISO): string => { 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 * Callback triggered when the payment of the subscription renewal was successful
*/ */
const onPaymentSuccess = (): void => { const onPaymentSuccess = (): void => {
onSuccess(t('app.admin.renew_subscription_modal.renew_success'), expirationDate); onSuccess(t('app.admin.renew_modal.renew_success'), expirationDate);
toggleModal(); toggleModal();
}; };
@ -108,25 +108,25 @@ const RenewModal: React.FC<RenewModalProps> = ({ isOpen, toggleModal, subscripti
toggleModal={toggleModal} toggleModal={toggleModal}
width={ModalSize.large} width={ModalSize.large}
className="renew-modal" className="renew-modal"
title={t('app.admin.renew_subscription_modal.renew_subscription')} title={t('app.admin.renew_modal.renew_subscription')}
confirmButton={t('app.admin.renew_subscription_modal.renew')} confirmButton={t('app.admin.renew_modal.renew')}
onConfirm={toggleLocalPaymentModal} onConfirm={toggleLocalPaymentModal}
closeButton> closeButton>
<FabAlert level="danger" className="conditions"> <FabAlert level="danger" className="conditions">
<p>{t('app.admin.renew_subscription_modal.renew_subscription_info')}</p> <p>{t('app.admin.renew_modal.renew_subscription_info')}</p>
<p>{t('app.admin.renew_subscription_modal.credits_will_be_reset')}</p> <p>{t('app.admin.renew_modal.credits_will_be_reset')}</p>
</FabAlert> </FabAlert>
<div className="form-and-payment"> <div className="form-and-payment">
<form className="configuration-form"> <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" <FabInput id="current_expiration"
defaultValue={formatDateTime(subscription.expired_at)} defaultValue={formatDateTime(subscription.expired_at)}
readOnly /> 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" <FabInput id="new_start"
defaultValue={formatDateTime(subscription.expired_at)} defaultValue={formatDateTime(subscription.expired_at)}
readOnly /> 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" <FabInput id="new_expiration"
defaultValue={formatDateTime(expirationDate)} defaultValue={formatDateTime(expirationDate)}
readOnly/> readOnly/>
@ -135,7 +135,7 @@ const RenewModal: React.FC<RenewModalProps> = ({ isOpen, toggleModal, subscripti
{subscription.plan.monthly_payment && <SelectSchedule show selected={scheduleRequired} onChange={setScheduleRequired} />} {subscription.plan.monthly_payment && <SelectSchedule show selected={scheduleRequired} onChange={setScheduleRequired} />}
{price?.schedule && <PaymentScheduleSummary schedule={price.schedule as PaymentSchedule} />} {price?.schedule && <PaymentScheduleSummary schedule={price.schedule as PaymentSchedule} />}
{price && !price?.schedule && <div className="one-go-payment"> {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> <span>{FormatLib.price(price.price)}</span>
</div>} </div>}
</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 * 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 { t } = useTranslation('admin');
const [selectedPlan, setSelectedPlan] = useState<Plan>(null); 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 { useTranslation } from 'react-i18next';
import { ProofOfIdentityType } from '../../models/proof-of-identity-type'; import { ProofOfIdentityType } from '../../models/proof-of-identity-type';
interface ProofOfIdentityRefusalFormProps { interface SupportingDocumentsRefusalFormProps {
proofOfIdentityTypes: Array<ProofOfIdentityType>, proofOfIdentityTypes: Array<ProofOfIdentityType>,
onChange: (field: string, value: string | Array<number>) => void, 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 { t } = useTranslation('admin');
const [values, setValues] = useState<Array<number>>([]); const [values, setValues] = useState<Array<number>>([]);
const [message, setMessage] = useState<string>(''); 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 handleMessageChange = (e: BaseSyntheticEvent): void => {
const { value } = e.target; const { value } = e.target;
@ -26,10 +26,9 @@ export const ProofOfIdentityRefusalForm: React.FC<ProofOfIdentityRefusalFormProp
}; };
/** /**
* Callback triggered when a checkbox is ticked or unticked. * Callback triggered when the document type checkbox is ticked or unticked.
* This function construct the resulting string, by adding or deleting the provided option identifier.
*/ */
const handleProofOfIdnentityTypesChange = (value: number) => { const handleTypeSelectionChange = (value: number) => {
return (event: BaseSyntheticEvent) => { return (event: BaseSyntheticEvent) => {
let newValues: Array<number>; let newValues: Array<number>;
if (event.target.checked) { 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) => { const isChecked = (typeId: number) => {
return values.includes(value); return values.includes(typeId);
}; };
return ( return (
<div className="proof-of-identity-type-form"> <div className="supporting-documents-refusal-form">
<form name="proofOfIdentityRefusalForm"> <form name="proofOfIdentityRefusalForm">
<div> <div>
{proofOfIdentityTypes.map(type => <div key={type.id} className=""> {proofOfIdentityTypes.map(type => <div key={type.id}>
<label htmlFor={`checkbox-${type.id}`}>{type.name}</label> <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> </div>
<div className="proof-of-identity-refusal-comment-textarea m-t"> <div className="refusal-comment">
<label htmlFor="proof-of-identity-refusal-comment">{t('app.admin.members_edit.proof_of_identity_refusal_comment')}</label> <label htmlFor="proof-of-identity-refusal-comment">
{t('app.admin.supporting_documents_refusal_form.refusal_comment')}
</label>
<textarea <textarea
id="proof-of-identity-refusal-comment" id="proof-of-identity-refusal-comment"
value={message} 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} onChange={handleMessageChange}
style={{ width: '100%' }} style={{ width: '100%' }}
rows={5} 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 { ProofOfIdentityType } from '../../models/proof-of-identity-type';
import { Group } from '../../models/group'; import { Group } from '../../models/group';
interface ProofOfIdentityTypeFormProps { interface SupportingDocumentsTypeFormProps {
groups: Array<Group>, groups: Array<Group>,
proofOfIdentityType?: ProofOfIdentityType, proofOfIdentityType?: ProofOfIdentityType,
onChange: (field: string, value: string | Array<number>) => void, onChange: (field: string, value: string | Array<number>) => void,
@ -18,13 +18,13 @@ interface ProofOfIdentityTypeFormProps {
type selectOption = { value: number, label: string }; 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'); const { t } = useTranslation('admin');
/** /**
* Convert all themes to the react-select format * Convert all groups to the react-select format
*/ */
const buildOptions = (): Array<selectOption> => { const buildOptions = (): Array<selectOption> => {
return groups.map(t => { 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 groupsValues = (): Array<selectOption> => {
const res = []; const res = [];
@ -63,23 +63,23 @@ export const ProofOfIdentityTypeForm: React.FC<ProofOfIdentityTypeFormProps> = (
}; };
return ( return (
<div className="proof-of-identity-type-form"> <div className="supporting-documents-type-form">
<div className="proof-of-identity-type-form-info"> <div className="info-area">
{t('app.admin.settings.compte.proof_of_identity_type_form_info')} {t('app.admin.settings.account.supporting_documents_type_form.type_form_info')}
</div> </div>
<form name="proofOfIdentityTypeForm"> <form name="proofOfIdentityTypeForm">
<div className="proof-of-identity-type-select m-t"> <div className="field">
<Select defaultValue={groupsValues()} <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} onChange={handleGroupsChange}
options={buildOptions()} options={buildOptions()}
isMulti /> isMulti />
</div> </div>
<div className="proof-of-identity-type-input m-t"> <div className="field">
<FabInput id="proof_of_identity_type_name" <FabInput id="proof_of_identity_type_name"
icon={<i className="fa fa-edit" />} icon={<i className="fa fa-edit" />}
defaultValue={proofOfIdentityType?.name || ''} 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} onChange={handleNameChange}
debounce={200} debounce={200}
required/> 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 { FormMultiSelect } from '../form/form-multi-select';
import ProfileCustomFieldAPI from '../../api/profile-custom-field'; import ProfileCustomFieldAPI from '../../api/profile-custom-field';
import { ProfileCustomField } from '../../models/profile-custom-field'; import { ProfileCustomField } from '../../models/profile-custom-field';
import { SettingName } from '../../models/setting';
import SettingAPI from '../../api/setting';
declare const Application: IApplication; declare const Application: IApplication;
@ -68,6 +70,7 @@ export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size,
const [groups, setGroups] = useState<selectOption[]>([]); const [groups, setGroups] = useState<selectOption[]>([]);
const [termsAndConditions, setTermsAndConditions] = useState<CustomAsset>(null); const [termsAndConditions, setTermsAndConditions] = useState<CustomAsset>(null);
const [profileCustomFields, setProfileCustomFields] = useState<ProfileCustomField[]>([]); const [profileCustomFields, setProfileCustomFields] = useState<ProfileCustomField[]>([]);
const [requiredFieldsSettings, setRequiredFieldsSettings] = useState<Map<SettingName, string>>(new Map());
useEffect(() => { useEffect(() => {
AuthProviderAPI.active().then(data => { 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); setValue('invoicing_profile_attributes.user_profile_custom_fields_attributes', userProfileCustomFields);
}).catch(error => onError(error)); }).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} register={register}
label={t('app.shared.user_profile_form.date_of_birth')} label={t('app.shared.user_profile_form.date_of_birth')}
disabled={isDisabled} disabled={isDisabled}
rules={{ required: true }}
type="date" /> type="date" />
<FormInput id="profile_attributes.phone" <FormInput id="profile_attributes.phone"
register={register} register={register}
@ -209,7 +216,8 @@ export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size,
pattern: { pattern: {
value: phoneRegex, value: phoneRegex,
message: t('app.shared.user_profile_form.phone_number_invalid') message: t('app.shared.user_profile_form.phone_number_invalid')
} },
required: requiredFieldsSettings.get(SettingName.PhoneRequired) === 'true'
}} }}
disabled={isDisabled} disabled={isDisabled}
formState={formState} formState={formState}
@ -222,6 +230,7 @@ export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size,
<FormInput id="invoicing_profile_attributes.address_attributes.address" <FormInput id="invoicing_profile_attributes.address_attributes.address"
register={register} register={register}
disabled={isDisabled} disabled={isDisabled}
rules={{ required: requiredFieldsSettings.get(SettingName.AddressRequired) === 'true' }}
label={t('app.shared.user_profile_form.address')} /> label={t('app.shared.user_profile_form.address')} />
</div> </div>
</div> </div>

View File

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

View File

@ -929,9 +929,6 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
const initialize = function () { const initialize = function () {
CSRF.setMetaTags(); 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 // the user subscription
if (($scope.user.subscribed_plan != null) && ($scope.user.subscription != null)) { if (($scope.user.subscribed_plan != null) && ($scope.user.subscription != null)) {
$scope.subscription = $scope.user.subscription; $scope.subscription = $scope.user.subscription;

View File

@ -72,6 +72,7 @@ Application.Controllers.controller('EventsController', ['$scope', '$state', 'Eve
// reinitialize results datasets // reinitialize results datasets
$scope.page = 1; $scope.page = 1;
$scope.eventsGroupByMonth = {}; $scope.eventsGroupByMonth = {};
$scope.featuredEevent = null;
$scope.events = []; $scope.events = [];
$scope.monthOrder = []; $scope.monthOrder = [];
$scope.noMoreResults = false; $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'); }; $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 */ /* PRIVATE SCOPE */
/** /**
@ -117,7 +128,8 @@ Application.Controllers.controller('EventsController', ['$scope', '$state', 'Eve
}); });
}); });
$scope.eventsGroupByMonth = Object.assign($scope.eventsGroupByMonth, eventsGroupedByMonth); $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') { if (typeof $scope.type === 'undefined') {
$scope.type = 'text'; $scope.type = 'text';
} }
// 'required' default to true
if (typeof $scope.required === 'undefined') {
$scope.required = true;
}
// The setting // The setting
$scope.setting = { $scope.setting = {
name: $scope.name, 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 { export interface AuthenticationProviderMapping {
id?: number, id?: number,
_destroy?: boolean,
local_model: 'user' | 'profile', local_model: 'user' | 'profile',
local_field: string, local_field: string,
api_field: string, api_field: string,

View File

@ -4,8 +4,8 @@ export interface ProofOfIdentityFileIndexFilter {
} }
export interface ProofOfIdentityFile { export interface ProofOfIdentityFile {
id: number, id?: number,
attachment: string, attachment?: string,
user_id: number, user_id?: number,
proof_of_identity_file_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.interest': 'profile.interest',
'profile_attributes.software_mastered': 'profile.software_mastered', 'profile_attributes.software_mastered': 'profile.software_mastered',
is_allow_contact: 'user.is_allow_contact', 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); }, ...socialMappings);

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