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:
commit
9f7504dfe9
16
.eslintrc
16
.eslintrc
@ -19,7 +19,7 @@
|
||||
"$": true,
|
||||
"KeyboardEvent": true
|
||||
},
|
||||
"plugins": ["html-erb"],
|
||||
"plugins": ["html-erb", "fabmanager"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/*.ts", "**/*.tsx"],
|
||||
@ -46,6 +46,20 @@
|
||||
"react/prop-types": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["app/frontend/src/javascript/components/**/*.tsx"],
|
||||
"rules": {
|
||||
"import/no-default-export": "error",
|
||||
"import/no-unused-modules": "error",
|
||||
"fabmanager/component-class-named-as-component": ["error", { "ignoreAbstractKeyword": true }],
|
||||
"fabmanager/component-named-like-file": "error",
|
||||
"fabmanager/component-documentation": "warn",
|
||||
"fabmanager/component-methods-documentation": "warn",
|
||||
"fabmanager/no-bootstrap": "error",
|
||||
"fabmanager/no-utilities": "error",
|
||||
"fabmanager/scoped-translation": ["error", { "ignoreAbstractKeyword": false }]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["app/frontend/src/javascript/models/**/*.ts"],
|
||||
"rules": {
|
||||
|
33
CHANGELOG.md
33
CHANGELOG.md
@ -2,6 +2,39 @@
|
||||
|
||||
## next deploy
|
||||
|
||||
## v5.4.5 2022 June 27
|
||||
|
||||
- Feature the next event in the event page
|
||||
- Documentation for installing behind a proxy
|
||||
- Ability to install behind a proxy
|
||||
- Improved docker image building time
|
||||
- Use relative paths in mount scripts
|
||||
- Run the docker image with the system user
|
||||
- During the setup, autoconfigure the main domain
|
||||
- During the setup, ask to set ALLOW_INSECURE_HTTP if DEFAULT_PROTOCOL was set to http
|
||||
- Override angular currency filter, use Intl.NumberFormat to format amount
|
||||
- Added some eslint rules to validate react components code style
|
||||
- Fixed all react components code according to eslint rules
|
||||
- Renamed proof-of-identity to supporting-documents in react components and in end-user strings
|
||||
- Use bat to display coloured documentation of environment variables during setup
|
||||
- Check the minimum docker version (20.10) during setup and upgrade
|
||||
- Fix a bug: when email was mapped from SSO provided as empty string -> unable to merge account
|
||||
- Fix a bug: when an empty data was retured by the SSO, unable to edit it
|
||||
- Fix a bug: user can change his group in the profile completion page, even if mapped from the SSO
|
||||
- Fix a bug: the birthdate was not marked as required, in the profile edition form
|
||||
- Fix a bug: when the phone or the address were required, they were not marked as this, in the profile edition form
|
||||
- Fix a bug: the birthday was not shown in user edition form
|
||||
- Fix a bug: canceled event label's translation
|
||||
- Fix a bug: unable to set the twitter input empty
|
||||
- Fix a bug: unable to edit an event
|
||||
- Fix a bug: times are not shown in admin/events monitoring page
|
||||
- Fix a bug: unable to generate the secret key base during the setup
|
||||
- Fix a bug: error message during the setup: the input device is not a TTY
|
||||
- Fix a bug: when Fab-manager was installed as non-root user, unable to compile the assets during the upgrade
|
||||
- Fix a bug: unable to remove an SSO data mapping field once saved
|
||||
- Fix a bug: unable to update the user profile after toggling the organization switch twice
|
||||
- [TODO DEPLOY] `\curl -sSL https://raw.githubusercontent.com/sleede/fab-manager/master/scripts/set-docker-user.sh | bash`
|
||||
|
||||
## v5.4.4 2022 June 8
|
||||
|
||||
- Check shopping cart items are valid before online payment
|
||||
|
34
Dockerfile
34
Dockerfile
@ -44,14 +44,19 @@ RUN bundle config --global frozen 1
|
||||
|
||||
# Install gems in a cache efficient way
|
||||
WORKDIR /tmp
|
||||
COPY Gemfile /tmp/
|
||||
COPY Gemfile.lock /tmp/
|
||||
COPY Gemfile* /tmp/
|
||||
RUN bundle config set --local without 'development test doc' && bundle install && bundle binstubs --all
|
||||
|
||||
# Prepare the application directories
|
||||
RUN mkdir -p /var/log/supervisor && \
|
||||
mkdir -p /usr/src/app/tmp/sockets && \
|
||||
mkdir -p /usr/src/app/tmp/pids
|
||||
mkdir -p /usr/src/app/tmp/pids && \
|
||||
mkdir -p /usr/src/app/tmp/cache && \
|
||||
mkdir -p /usr/src/app/log && \
|
||||
mkdir -p /usr/src/app/node_modules && \
|
||||
mkdir -p /usr/src/app/public/api && \
|
||||
chmod -R a+w /usr/src/app && \
|
||||
chmod -R a+w /var/run
|
||||
|
||||
# Install Javascript packages
|
||||
WORKDIR /usr/src/app
|
||||
@ -65,23 +70,24 @@ RUN apk del .build-deps && \
|
||||
rm -rf /tmp/* \
|
||||
/var/tmp/* \
|
||||
/var/cache/apk/* \
|
||||
/usr/lib/ruby/gems/*/cache/*
|
||||
/usr/lib/ruby/gems/*/cache/* && \
|
||||
chmod -R a+w /usr/src/app/node_modules
|
||||
|
||||
# Copy source files
|
||||
COPY docker/database.yml /usr/src/app/config/database.yml
|
||||
COPY . /usr/src/app
|
||||
|
||||
# Volumes (the folders are created by setup.sh)
|
||||
VOLUME /usr/src/app/invoices
|
||||
VOLUME /usr/src/app/payment_schedules
|
||||
VOLUME /usr/src/app/exports
|
||||
VOLUME /usr/src/app/imports
|
||||
VOLUME /usr/src/app/public
|
||||
VOLUME /usr/src/app/public/uploads
|
||||
VOLUME /usr/src/app/public/packs
|
||||
VOLUME /usr/src/app/accounting
|
||||
VOLUME /usr/src/app/proof_of_identity_files
|
||||
VOLUME /var/log/supervisor
|
||||
VOLUME /usr/src/app/invoices \
|
||||
/usr/src/app/payment_schedules \
|
||||
/usr/src/app/exports \
|
||||
/usr/src/app/imports \
|
||||
/usr/src/app/public \
|
||||
/usr/src/app/public/uploads \
|
||||
/usr/src/app/public/packs \
|
||||
/usr/src/app/accounting \
|
||||
/usr/src/app/proof_of_identity_files \
|
||||
/var/log/supervisor
|
||||
|
||||
# Expose port 3000 to the Docker host, so we can access it from the outside
|
||||
EXPOSE 3000
|
||||
|
2
Procfile
2
Procfile
@ -1,3 +1,3 @@
|
||||
#web: bundle exec rails server puma -p $PORT
|
||||
web: bundle exec rails server puma -p $PORT
|
||||
worker: bundle exec sidekiq -C ./config/sidekiq.yml
|
||||
webpack: bin/webpacker-dev-server
|
||||
|
@ -267,7 +267,7 @@ class API::MembersController < API::ApiController
|
||||
:dailymotion, :github, :echosciences, :pinterest, :lastfm, :flickr,
|
||||
user_avatar_attributes: %i[id attachment destroy]],
|
||||
invoicing_profile_attributes: [
|
||||
:id,
|
||||
:id, :organization,
|
||||
address_attributes: %i[id address],
|
||||
organization_attributes: [:id, :name, address_attributes: %i[id address]],
|
||||
user_profile_custom_fields_attributes: %i[id value invoicing_profile_id profile_custom_field_id]
|
||||
@ -282,7 +282,7 @@ class API::MembersController < API::ApiController
|
||||
:dailymotion, :github, :echosciences, :pinterest, :lastfm, :flickr,
|
||||
user_avatar_attributes: %i[id attachment destroy]],
|
||||
invoicing_profile_attributes: [
|
||||
:id,
|
||||
:id, :organization,
|
||||
address_attributes: %i[id address],
|
||||
organization_attributes: [:id, :name, address_attributes: %i[id address]],
|
||||
user_profile_custom_fields_attributes: %i[id value invoicing_profile_id profile_custom_field_id]
|
||||
|
@ -14,4 +14,5 @@ These components must be written using the following conventions:
|
||||
- Depending on if we want to use the `<Suspense>` wrapper or not, we can export the component directly or wrap it in a `<Loader>` wrapper.
|
||||
- When a component is used in angularJS, the wrapper is required. The component must be named like `const Foo` (no export if not used in React) and must have a `const FooWrapper` at the end of its file, which wraps the component in a `<Loader>`.
|
||||
- Translations must be grouped per component. For example, the `FooBar` component must have its translations in the `config/locales/app.$SCOPE.en.yml` file, under the `foo_bar` key.
|
||||
- Most of these rules are validated by eslint-plugin-fabmanager. Please ensure you write eslint valid code, and think twice you have a very good reason before disabling any rule.
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
/**
|
||||
* This is a compatibility wrapper to allow usage of react-switch inside of the angular.js app
|
||||
* This is a compatibility wrapper to allow usage of react-switch inside the angular.js app
|
||||
*/
|
||||
import Switch from 'react-switch';
|
||||
import { react2angular } from 'react2angular';
|
||||
|
@ -1,9 +1,9 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { UseFormRegister, useFieldArray, ArrayPath, useWatch, Path } from 'react-hook-form';
|
||||
import { UseFormRegister, useFieldArray, ArrayPath, useWatch, Path, FieldPathValue } from 'react-hook-form';
|
||||
import { FieldValues } from 'react-hook-form/dist/types/fields';
|
||||
import AuthProviderAPI from '../../api/auth-provider';
|
||||
import { AuthenticationProviderMapping, MappingFields, mappingType, ProvidableType } from '../../models/authentication-provider';
|
||||
import { Control, UseFormSetValue } from 'react-hook-form/dist/types/form';
|
||||
import { Control, UnpackNestedValue, UseFormSetValue } from 'react-hook-form/dist/types/form';
|
||||
import { FormSelect } from '../form/form-select';
|
||||
import { FormInput } from '../form/form-input';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -96,6 +96,31 @@ export const DataMappingForm = <TFieldValues extends FieldValues, TContext exten
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove the data whom index is provided: mark it as "to destroy" or simply remove it if it was unsaved
|
||||
*/
|
||||
const removeMapping = (index: number): void => {
|
||||
if (currentFormValues[index].id) {
|
||||
setValue(
|
||||
`auth_provider_mappings_attributes.${index}._destroy` as Path<TFieldValues>,
|
||||
true as UnpackNestedValue<FieldPathValue<TFieldValues, Path<TFieldValues>>>
|
||||
);
|
||||
} else {
|
||||
remove(index);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a className based on the current mapping-item status
|
||||
*/
|
||||
const itemStatus = (index: number): string => {
|
||||
if (currentFormValues[index]?.id) {
|
||||
if (currentFormValues[index]._destroy) return 'destroyed-item';
|
||||
return 'saved-item';
|
||||
}
|
||||
return 'new-item';
|
||||
};
|
||||
|
||||
// fetch the mapping data from the API on mount
|
||||
useEffect(() => {
|
||||
AuthProviderAPI.mappingFields().then((data) => {
|
||||
@ -114,7 +139,7 @@ export const DataMappingForm = <TFieldValues extends FieldValues, TContext exten
|
||||
</FabButton>
|
||||
</div>
|
||||
{fields.map((item, index) => (
|
||||
<div key={item.id} className="mapping-item">
|
||||
<div key={item.id} className={`mapping-item ${itemStatus(index)}`}>
|
||||
<div className="inputs">
|
||||
<FormInput id={`auth_provider_mappings_attributes.${index}.id`} register={register} type="hidden" />
|
||||
<div className="local-data">
|
||||
@ -141,7 +166,7 @@ export const DataMappingForm = <TFieldValues extends FieldValues, TContext exten
|
||||
onClick={toggleTypeMappingModal(index)}
|
||||
disabled={getField(output, index) === undefined}
|
||||
tooltip={t('app.admin.authentication.data_mapping_form.data_mapping')} />
|
||||
<FabButton icon={<i className="fa fa-trash" />} onClick={() => remove(index)} className="delete-button" />
|
||||
<FabButton icon={<i className="fa fa-trash" />} onClick={() => removeMapping(index)} className="delete-button" />
|
||||
<TypeMappingModal model={getModel(output, index)}
|
||||
field={getField(output, index)}
|
||||
type={getDataType(output, index)}
|
||||
|
@ -13,6 +13,10 @@ interface Oauth2DataMappingFormProps<TFieldValues, TContext extends object> {
|
||||
index: number,
|
||||
}
|
||||
|
||||
/**
|
||||
* Partial form to set the data mapping for an OAuth 2.0 provider.
|
||||
* The data mapping is the way to bind data from the authentication provider API to the Fab-manager's database
|
||||
*/
|
||||
export const Oauth2DataMappingForm = <TFieldValues extends FieldValues, TContext extends object>({ register, control, index }: Oauth2DataMappingFormProps<TFieldValues, TContext>) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
|
@ -16,6 +16,10 @@ interface OpenidConnectDataMappingFormProps<TFieldValues> {
|
||||
index: number,
|
||||
}
|
||||
|
||||
/**
|
||||
* Partial form to set the data mapping for an OpenID Connect provider.
|
||||
* The data mapping is the way to bind data from the OIDC claims to the Fab-manager's database
|
||||
*/
|
||||
export const OpenidConnectDataMappingForm = <TFieldValues extends FieldValues>({ register, setValue, currentFormValues, index }: OpenidConnectDataMappingFormProps<TFieldValues>) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
@ -40,6 +44,7 @@ export const OpenidConnectDataMappingForm = <TFieldValues extends FieldValues>({
|
||||
const model = currentFormValues[index]?.local_model;
|
||||
const field = currentFormValues[index]?.local_field;
|
||||
const configuration = standardConfiguration[`${model}.${field}`];
|
||||
if (configuration) {
|
||||
setValue(
|
||||
`auth_provider_mappings_attributes.${index}.api_field` as Path<TFieldValues>,
|
||||
configuration.api_field as UnpackNestedValue<FieldPathValue<TFieldValues, Path<TFieldValues>>>
|
||||
@ -52,6 +57,7 @@ export const OpenidConnectDataMappingForm = <TFieldValues extends FieldValues>({
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -20,6 +20,9 @@ interface OpenidConnectFormProps<TFieldValues, TContext extends object> {
|
||||
setValue: UseFormSetValue<TFieldValues>,
|
||||
}
|
||||
|
||||
/**
|
||||
* Partial form to fill the OpenID Connect (OIDC) settings for a new/existing authentication provider.
|
||||
*/
|
||||
export const OpenidConnectForm = <TFieldValues extends FieldValues, TContext extends object>({ register, control, currentFormValues, formState, setValue }: OpenidConnectFormProps<TFieldValues, TContext>) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
|
@ -6,7 +6,7 @@ type inputType = string|number|readonly string [];
|
||||
interface FabInputProps {
|
||||
id: string,
|
||||
onChange?: (value: inputType, validity?: ValidityState) => void,
|
||||
defaultValue: inputType,
|
||||
defaultValue?: inputType,
|
||||
icon?: ReactNode,
|
||||
addOn?: ReactNode,
|
||||
addOnClassName?: string,
|
||||
|
@ -45,7 +45,7 @@ export const FabModal: React.FC<FabModalProps> = ({ title, isOpen, toggleModal,
|
||||
className={`fab-modal fab-modal-${width} ${className}`}
|
||||
overlayClassName="fab-modal-overlay"
|
||||
onRequestClose={toggleModal}>
|
||||
{closeButton && <FabButton className="modal-btn--close" onClick={toggleModal}>{t('app.shared.buttons.close')}</FabButton>}
|
||||
{closeButton && <FabButton className="modal-btn--close" onClick={toggleModal}>{t('app.shared.fab_modal.close')}</FabButton>}
|
||||
<div className="fab-modal-header">
|
||||
{!customHeader && <h1>{ title }</h1>}
|
||||
{customHeader && customHeader}
|
||||
|
28
app/frontend/src/javascript/components/base/fab-panel.tsx
Normal file
28
app/frontend/src/javascript/components/base/fab-panel.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -15,9 +15,9 @@ interface LabelledInputProps {
|
||||
*/
|
||||
export const LabelledInput: React.FC<LabelledInputProps> = ({ id, type, label, value, onChange }) => {
|
||||
return (
|
||||
<div className="input-with-label">
|
||||
<label className="label" htmlFor={id}>{label}</label>
|
||||
<input className="input" id={id} type={type} value={value} onChange={onChange} />
|
||||
<div className="labelled-input">
|
||||
<label htmlFor={id}>{label}</label>
|
||||
<input id={id} type={type} value={value} onChange={onChange} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -32,7 +32,7 @@ export interface FabTextEditorRef {
|
||||
*/
|
||||
export const FabTextEditor: React.ForwardRefRenderFunction<FabTextEditorRef, FabTextEditorProps> = ({ paragraphTools, content, limit = 400, video, image, onChange, placeholder, error, disabled = false }, ref: RefObject<FabTextEditorRef>) => {
|
||||
const { t } = useTranslation('shared');
|
||||
const placeholderText = placeholder || t('app.shared.text_editor.text_placeholder');
|
||||
const placeholderText = placeholder || t('app.shared.text_editor.fab_text_editor.text_placeholder');
|
||||
// TODO: Add ctrl+click on link to visit
|
||||
|
||||
const editorRef: React.MutableRefObject<Editor | null> = useRef(null);
|
||||
@ -66,7 +66,7 @@ export const FabTextEditor: React.ForwardRefRenderFunction<FabTextEditorRef, Fab
|
||||
Iframe,
|
||||
Image.configure({
|
||||
HTMLAttributes: {
|
||||
class: 'fab-textEditor-image'
|
||||
class: 'fab-text-editor-image'
|
||||
}
|
||||
})
|
||||
],
|
||||
@ -85,14 +85,14 @@ export const FabTextEditor: React.ForwardRefRenderFunction<FabTextEditorRef, Fab
|
||||
editorRef.current = editor;
|
||||
|
||||
return (
|
||||
<div className={`fab-textEditor ${disabled && 'is-disabled'}`}>
|
||||
<div className={`fab-text-editor ${disabled && 'is-disabled'}`}>
|
||||
<MenuBar editor={editor} paragraphTools={paragraphTools} video={video} image={image} disabled={disabled} />
|
||||
<EditorContent editor={editor} />
|
||||
<div className="fab-textEditor-character-count">
|
||||
<div className="fab-text-editor-character-count">
|
||||
{editor?.storage.characterCount.characters()} / {limit}
|
||||
</div>
|
||||
{error &&
|
||||
<div className="fab-textEditor-error">
|
||||
<div className="fab-text-editor-error">
|
||||
<WarningOctagon size={24} />
|
||||
<p className="">{error}</p>
|
||||
</div>
|
||||
@ -101,4 +101,5 @@ export const FabTextEditor: React.ForwardRefRenderFunction<FabTextEditorRef, Fab
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default forwardRef(FabTextEditor);
|
||||
|
@ -18,6 +18,7 @@ declare module '@tiptap/core' {
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default Node.create<IframeOptions>({
|
||||
name: 'iframe',
|
||||
|
||||
@ -29,7 +30,7 @@ export default Node.create<IframeOptions>({
|
||||
return {
|
||||
allowFullscreen: true,
|
||||
HTMLAttributes: {
|
||||
class: 'fab-textEditor-video'
|
||||
class: 'fab-text-editor-video'
|
||||
}
|
||||
};
|
||||
},
|
||||
|
@ -141,7 +141,7 @@ export const MenuBar: React.FC<MenuBarProps> = ({ editor, paragraphTools, video,
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`fab-textEditor-menu ${disabled ? 'fab-textEditor-menu--disabled' : ''}`}>
|
||||
<div className={`fab-text-editor-menu ${disabled ? 'fab-text-editor-menu--disabled' : ''}`}>
|
||||
{ paragraphTools &&
|
||||
(<>
|
||||
<button
|
||||
@ -168,7 +168,7 @@ export const MenuBar: React.FC<MenuBarProps> = ({ editor, paragraphTools, video,
|
||||
>
|
||||
<Quotes size={24} />
|
||||
</button>
|
||||
<span className='divider'></span>
|
||||
<span className='menu-divider'></span>
|
||||
</>)
|
||||
}
|
||||
<button
|
||||
@ -203,7 +203,7 @@ export const MenuBar: React.FC<MenuBarProps> = ({ editor, paragraphTools, video,
|
||||
>
|
||||
<LinkSimpleHorizontal size={24} />
|
||||
</button>
|
||||
{ (video || image) && <span className='divider'></span> }
|
||||
{ (video || image) && <span className='menu-divider'></span> }
|
||||
{ video &&
|
||||
(<>
|
||||
<button
|
||||
@ -228,19 +228,19 @@ export const MenuBar: React.FC<MenuBarProps> = ({ editor, paragraphTools, video,
|
||||
}
|
||||
</div>
|
||||
|
||||
<div ref={ref} className={`fab-textEditor-subMenu ${submenu ? 'is-active' : ''}`}>
|
||||
<div ref={ref} className={`fab-text-editor-subMenu ${submenu ? 'is-active' : ''}`}>
|
||||
{ submenu === 'link' &&
|
||||
(<>
|
||||
<h6>{t('app.shared.text_editor.add_link')}</h6>
|
||||
<h6>{t('app.shared.text_editor.menu_bar.add_link')}</h6>
|
||||
<div>
|
||||
<input value={url.href} onChange={linkUrlChange} onKeyDown={handleEnter} type="text" placeholder={t('app.shared.text_editor.link_placeholder')} />
|
||||
<input value={url.href} onChange={linkUrlChange} onKeyDown={handleEnter} type="text" placeholder={t('app.shared.text_editor.menu_bar.link_placeholder')} />
|
||||
<button type='button' onClick={unsetLink}>
|
||||
<Trash size={24} />
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<label className='tab'>
|
||||
<p>{t('app.shared.text_editor.new_tab')}</p>
|
||||
<p>{t('app.shared.text_editor.menu_bar.new_tab')}</p>
|
||||
<input type="checkbox" onChange={toggleTarget} checked={url.target === '_blank'} />
|
||||
<span className='switch'></span>
|
||||
</label>
|
||||
@ -252,14 +252,14 @@ export const MenuBar: React.FC<MenuBarProps> = ({ editor, paragraphTools, video,
|
||||
}
|
||||
{ submenu === 'video' &&
|
||||
(<>
|
||||
<h6>{t('app.shared.text_editor.add_video')}</h6>
|
||||
<h6>{t('app.shared.text_editor.menu_bar.add_video')}</h6>
|
||||
<select name="provider" onChange={handleSelect}>
|
||||
<option value="youtube">YouTube</option>
|
||||
<option value="vimeo">Vimeo</option>
|
||||
<option value="dailymotion">Dailymotion</option>
|
||||
</select>
|
||||
<div>
|
||||
<input type="text" onChange={videoUrlChange} placeholder={t('app.shared.text_editor.url_placeholder')} />
|
||||
<input type="text" onChange={videoUrlChange} placeholder={t('app.shared.text_editor.menu_bar.url_placeholder')} />
|
||||
<button type='button' onClick={() => addIframe()}>
|
||||
<CheckCircle size={24} />
|
||||
</button>
|
||||
@ -268,9 +268,9 @@ export const MenuBar: React.FC<MenuBarProps> = ({ editor, paragraphTools, video,
|
||||
}
|
||||
{ submenu === 'image' &&
|
||||
(<>
|
||||
<h6>{t('app.shared.text_editor.add_image')}</h6>
|
||||
<h6>{t('app.shared.text_editor.menu_bar.add_image')}</h6>
|
||||
<div>
|
||||
<input type="text" onChange={imageUrlChange} placeholder={t('app.shared.text_editor.url_placeholder')} />
|
||||
<input type="text" onChange={imageUrlChange} placeholder={t('app.shared.text_editor.menu_bar.url_placeholder')} />
|
||||
<button type='button' onClick={() => addImage()}>
|
||||
<CheckCircle size={24} />
|
||||
</button>
|
||||
|
138
app/frontend/src/javascript/components/events/event-card.tsx
Normal file
138
app/frontend/src/javascript/components/events/event-card.tsx
Normal 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']));
|
@ -2,11 +2,11 @@ import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Select from 'react-select';
|
||||
import { react2angular } from 'react2angular';
|
||||
import { Loader } from './base/loader';
|
||||
import { Event } from '../models/event';
|
||||
import { EventTheme } from '../models/event-theme';
|
||||
import { IApplication } from '../models/application';
|
||||
import EventThemeAPI from '../api/event-theme';
|
||||
import { Loader } from '../base/loader';
|
||||
import { Event } from '../../models/event';
|
||||
import { EventTheme } from '../../models/event-theme';
|
||||
import { IApplication } from '../../models/application';
|
||||
import EventThemeAPI from '../../api/event-theme';
|
||||
|
||||
declare const Application: IApplication;
|
||||
|
||||
@ -24,7 +24,7 @@ type selectOption = { value: number, label: string };
|
||||
/**
|
||||
* This component shows a select input to edit the themes associated with the event
|
||||
*/
|
||||
const EventThemes: React.FC<EventThemesProps> = ({ event, onChange }) => {
|
||||
export const EventThemes: React.FC<EventThemesProps> = ({ event, onChange }) => {
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
const [themes, setThemes] = useState<Array<EventTheme>>([]);
|
||||
@ -77,10 +77,10 @@ const EventThemes: React.FC<EventThemesProps> = ({ event, onChange }) => {
|
||||
return (
|
||||
<div className="event-themes">
|
||||
{hasThemes() && <div className="event-themes--panel">
|
||||
<h3>{ t('app.shared.event.event_themes') }</h3>
|
||||
<h3>{ t('app.shared.event_themes.title') }</h3>
|
||||
<div className="content">
|
||||
<Select defaultValue={defaultValues()}
|
||||
placeholder={t('app.shared.event.select_theme')}
|
||||
placeholder={t('app.shared.event_themes.select_theme')}
|
||||
onChange={handleChange}
|
||||
options={buildOptions()}
|
||||
isMulti />
|
@ -2,7 +2,7 @@
|
||||
|
||||
This directory is holding the inputs components for usage within forms controlled by [React-hook-form](https://react-hook-form.com/).
|
||||
|
||||
All these components must have [props](https://reactjs.org/docs/components-and-props.html) that inherits from [FormComponent](../models/form-component.ts)
|
||||
All these components must have [props](https://reactjs.org/docs/components-and-props.html) that inherit from [FormComponent](../models/form-component.ts)
|
||||
or from [FormControlledComponent](../models/form-component.ts).
|
||||
|
||||
Please look at the existing components for examples.
|
||||
|
@ -40,7 +40,6 @@ export const AbstractFormItem = <TFieldValues extends FieldValues>({ id, label,
|
||||
|
||||
// Compose classnames from props
|
||||
const classNames = [
|
||||
'form-item',
|
||||
`${className || ''}`,
|
||||
`${isDirty && fieldError ? 'is-incorrect' : ''}`,
|
||||
`${isDirty && warning ? 'is-warned' : ''}`,
|
||||
@ -59,7 +58,7 @@ export const AbstractFormItem = <TFieldValues extends FieldValues>({ id, label,
|
||||
}
|
||||
|
||||
return (
|
||||
<label className={classNames} onClick={handleLabelClick}>
|
||||
<label className={`form-item ${classNames}`} onClick={handleLabelClick}>
|
||||
{label && <div className='form-item-header'>
|
||||
<p>{label}</p>
|
||||
{tooltip && <div className="item-tooltip">
|
||||
|
@ -44,14 +44,13 @@ export const FormInput = <TFieldValues extends FieldValues, TInputType>({ id, re
|
||||
|
||||
// Compose classnames from props
|
||||
const classNames = [
|
||||
'form-input',
|
||||
`${className || ''}`,
|
||||
`${type === 'hidden' ? 'is-hidden' : ''}`
|
||||
].join(' ');
|
||||
|
||||
return (
|
||||
<AbstractFormItem id={id} formState={formState} label={label}
|
||||
className={classNames} tooltip={tooltip}
|
||||
className={`form-input ${classNames}`} tooltip={tooltip}
|
||||
disabled={disabled}
|
||||
rules={rules} error={error} warning={warning}>
|
||||
{icon && <span className="icon">{icon}</span>}
|
||||
|
@ -30,6 +30,9 @@ interface ChangeGroupProps {
|
||||
*/
|
||||
type selectOption = { value: number, label: string };
|
||||
|
||||
/**
|
||||
* Component to display the group of the provided user, and allow him to change his group.
|
||||
*/
|
||||
export const ChangeGroup: React.FC<ChangeGroupProps> = ({ user, onSuccess, onError, allowChange, className }) => {
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
|
@ -21,7 +21,7 @@ interface MachineCardProps {
|
||||
* This component is a box showing the picture of the given machine and two buttons: one to start the reservation process
|
||||
* and another to redirect the user to the machine description page.
|
||||
*/
|
||||
const MachineCardComponent: React.FC<MachineCardProps> = ({ user, machine, onShowMachine, onReserveMachine, onError, onSuccess, onLoginRequested, onEnrollRequested, canProposePacks }) => {
|
||||
const MachineCard: React.FC<MachineCardProps> = ({ user, machine, onShowMachine, onReserveMachine, onError, onSuccess, onLoginRequested, onEnrollRequested, canProposePacks }) => {
|
||||
const { t } = useTranslation('public');
|
||||
|
||||
// shall we display a loader to prevent double-clicking, while the machine details are loading?
|
||||
@ -40,6 +40,9 @@ const MachineCardComponent: React.FC<MachineCardProps> = ({ user, machine, onSho
|
||||
onShowMachine(machine);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the machine's picture or a placeholder
|
||||
*/
|
||||
const machinePicture = (): ReactNode => {
|
||||
if (!machine.machine_image) {
|
||||
return <div className="machine-picture no-picture" />;
|
||||
@ -82,10 +85,12 @@ const MachineCardComponent: React.FC<MachineCardProps> = ({ user, machine, onSho
|
||||
);
|
||||
};
|
||||
|
||||
export const MachineCard: React.FC<MachineCardProps> = ({ user, machine, onShowMachine, onReserveMachine, onError, onSuccess, onLoginRequested, onEnrollRequested, canProposePacks }) => {
|
||||
const MachineCardWrapper: React.FC<MachineCardProps> = (props) => {
|
||||
return (
|
||||
<Loader>
|
||||
<MachineCardComponent user={user} machine={machine} onShowMachine={onShowMachine} onReserveMachine={onReserveMachine} onError={onError} onSuccess={onSuccess} onLoginRequested={onLoginRequested} onEnrollRequested={onEnrollRequested} canProposePacks={canProposePacks} />
|
||||
<MachineCard {...props} />
|
||||
</Loader>
|
||||
);
|
||||
};
|
||||
|
||||
export { MachineCardWrapper as MachineCard };
|
||||
|
@ -12,6 +12,9 @@ interface MachinesFiltersProps {
|
||||
*/
|
||||
type selectOption = { value: boolean, label: string };
|
||||
|
||||
/**
|
||||
* Allows filtering on machines list
|
||||
*/
|
||||
export const MachinesFilters: React.FC<MachinesFiltersProps> = ({ onStatusSelected }) => {
|
||||
const { t } = useTranslation('public');
|
||||
|
||||
|
@ -24,7 +24,7 @@ interface MachinesListProps {
|
||||
/**
|
||||
* This component shows a list of all machines and allows filtering on that list.
|
||||
*/
|
||||
const MachinesList: React.FC<MachinesListProps> = ({ onError, onSuccess, onShowMachine, onReserveMachine, onLoginRequested, onEnrollRequested, user, canProposePacks }) => {
|
||||
export const MachinesList: React.FC<MachinesListProps> = ({ onError, onSuccess, onShowMachine, onReserveMachine, onLoginRequested, onEnrollRequested, user, canProposePacks }) => {
|
||||
// shown machines
|
||||
const [machines, setMachines] = useState<Array<Machine>>(null);
|
||||
// we keep the full list of machines, for filtering
|
||||
|
@ -3,11 +3,12 @@ import { FabModal } from '../base/fab-modal';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { HtmlTranslate } from '../base/html-translate';
|
||||
import FormatLib from '../../lib/format';
|
||||
import { TDateISO } from '../../typings/date-iso';
|
||||
|
||||
interface PendingTrainingModalProps {
|
||||
isOpen: boolean,
|
||||
toggleModal: () => void,
|
||||
nextReservation: Date,
|
||||
nextReservation: TDateISO,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -20,7 +21,7 @@ export const PendingTrainingModal: React.FC<PendingTrainingModalProps> = ({ isOp
|
||||
/**
|
||||
* Return the formatted localized date for the given date
|
||||
*/
|
||||
const formatDateTime = (date: Date): string => {
|
||||
const formatDateTime = (date: TDateISO): string => {
|
||||
return t('app.logged.pending_training_modal.DATE_TIME', { DATE: FormatLib.date(date), TIME: FormatLib.time(date) });
|
||||
};
|
||||
|
||||
|
@ -42,7 +42,7 @@ export const RequiredTrainingModal: React.FC<RequiredTrainingModalProps> = ({ is
|
||||
const header = (): ReactNode => {
|
||||
return (
|
||||
<div className="user-info">
|
||||
<Avatar userName={user?.name} avatar={user?.profile_attributes?.user_avatar_attributes?.attachment} />
|
||||
<Avatar userName={user?.name} avatar={user?.profile_attributes?.user_avatar_attributes?.attachment_url} />
|
||||
<span className="user-name">{user?.name}</span>
|
||||
</div>
|
||||
);
|
||||
|
@ -31,7 +31,7 @@ interface ReserveButtonProps {
|
||||
/**
|
||||
* Button component that makes the training verification before redirecting the user to the reservation calendar
|
||||
*/
|
||||
const ReserveButtonComponent: React.FC<ReserveButtonProps> = ({ currentUser, machineId, onLoginRequested, onLoadingStart, onLoadingEnd, onError, onSuccess, onReserveMachine, onEnrollRequested, className, children, canProposePacks }) => {
|
||||
const ReserveButton: React.FC<ReserveButtonProps> = ({ currentUser, machineId, onLoginRequested, onLoadingStart, onLoadingEnd, onError, onSuccess, onReserveMachine, onEnrollRequested, className, children, canProposePacks }) => {
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
const [machine, setMachine] = useState<Machine>(null);
|
||||
@ -183,14 +183,16 @@ const ReserveButtonComponent: React.FC<ReserveButtonProps> = ({ currentUser, mac
|
||||
);
|
||||
};
|
||||
|
||||
export const ReserveButton: React.FC<ReserveButtonProps> = ({ currentUser, machineId, onLoginRequested, onLoadingStart, onLoadingEnd, onError, onSuccess, onReserveMachine, onEnrollRequested, className, children, canProposePacks }) => {
|
||||
const ReserveButtonWrapper: React.FC<ReserveButtonProps> = (props) => {
|
||||
return (
|
||||
<Loader>
|
||||
<ReserveButtonComponent currentUser={currentUser} machineId={machineId} onError={onError} onSuccess={onSuccess} onLoadingStart={onLoadingStart} onLoadingEnd={onLoadingEnd} onReserveMachine={onReserveMachine} onLoginRequested={onLoginRequested} onEnrollRequested={onEnrollRequested} className={className} canProposePacks={canProposePacks}>
|
||||
{children}
|
||||
</ReserveButtonComponent>
|
||||
<ReserveButton {...props}>
|
||||
{props.children}
|
||||
</ReserveButton>
|
||||
</Loader>
|
||||
);
|
||||
};
|
||||
|
||||
Application.Components.component('reserveButton', react2angular(ReserveButton, ['currentUser', 'machineId', 'onLoadingStart', 'onLoadingEnd', 'onError', 'onSuccess', 'onReserveMachine', 'onLoginRequested', 'onEnrollRequested', 'className', 'canProposePacks']));
|
||||
export { ReserveButtonWrapper as ReserveButton };
|
||||
|
||||
Application.Components.component('reserveButton', react2angular(ReserveButtonWrapper, ['currentUser', 'machineId', 'onLoadingStart', 'onLoadingEnd', 'onError', 'onSuccess', 'onReserveMachine', 'onLoginRequested', 'onEnrollRequested', 'className', 'canProposePacks']));
|
||||
|
@ -40,29 +40,29 @@ export const PaymentScheduleSummary: React.FC<PaymentScheduleSummaryProps> = ({
|
||||
return (
|
||||
<div className="payment-schedule-summary">
|
||||
<div>
|
||||
<h4>{ t('app.shared.cart.your_payment_schedule') }</h4>
|
||||
<h4>{ t('app.shared.payment_schedule_summary.your_payment_schedule') }</h4>
|
||||
{hasEqualDeadlines() && <ul>
|
||||
<li>
|
||||
<span className="schedule-item-info">
|
||||
{t('app.shared.cart.NUMBER_monthly_payment_of_AMOUNT', { NUMBER: schedule.items.length, AMOUNT: FormatLib.price(schedule.items[0].amount) })}
|
||||
{t('app.shared.payment_schedule_summary.NUMBER_monthly_payment_of_AMOUNT', { NUMBER: schedule.items.length, AMOUNT: FormatLib.price(schedule.items[0].amount) })}
|
||||
</span>
|
||||
<span className="schedule-item-date">{t('app.shared.cart.first_debit')}</span>
|
||||
<span className="schedule-item-date">{t('app.shared.payment_schedule_summary.first_debit')}</span>
|
||||
</li>
|
||||
</ul>}
|
||||
{!hasEqualDeadlines() && <ul>
|
||||
<li>
|
||||
<span className="schedule-item-info">{t('app.shared.cart.monthly_payment_NUMBER', { NUMBER: 1 })}</span>
|
||||
<span className="schedule-item-info">{t('app.shared.payment_schedule_summary.monthly_payment_NUMBER', { NUMBER: 1 })}</span>
|
||||
<span className="schedule-item-price">{FormatLib.price(schedule.items[0].amount)}</span>
|
||||
<span className="schedule-item-date">{t('app.shared.cart.debit')}</span>
|
||||
<span className="schedule-item-date">{t('app.shared.payment_schedule_summary.debit')}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className="schedule-item-info">
|
||||
{t('app.shared.cart.NUMBER_monthly_payment_of_AMOUNT', { NUMBER: schedule.items.length - 1, AMOUNT: FormatLib.price(schedule.items[1].amount) })}
|
||||
{t('app.shared.payment_schedule_summary.NUMBER_monthly_payment_of_AMOUNT', { NUMBER: schedule.items.length - 1, AMOUNT: FormatLib.price(schedule.items[1].amount) })}
|
||||
</span>
|
||||
</li>
|
||||
</ul>}
|
||||
<button className="view-full-schedule" onClick={toggleFullScheduleModal}>{t('app.shared.cart.view_full_schedule')}</button>
|
||||
<FabModal title={t('app.shared.cart.your_payment_schedule')} isOpen={modal} toggleModal={toggleFullScheduleModal}>
|
||||
<button className="view-full-schedule" onClick={toggleFullScheduleModal}>{t('app.shared.payment_schedule_summary.view_full_schedule')}</button>
|
||||
<FabModal title={t('app.shared.payment_schedule_summary.your_payment_schedule')} isOpen={modal} toggleModal={toggleFullScheduleModal}>
|
||||
<ul className="full-schedule">
|
||||
{schedule.items.map(item => (
|
||||
<li key={String(item.due_date)}>
|
||||
|
@ -24,7 +24,7 @@ const PAGE_SIZE = 20;
|
||||
* This component shows a list of all payment schedules with their associated deadlines (aka. PaymentScheduleItem) and invoices
|
||||
* for the currentUser
|
||||
*/
|
||||
const PaymentSchedulesDashboard: React.FC<PaymentSchedulesDashboardProps> = ({ currentUser, onError, onCardUpdateSuccess }) => {
|
||||
export const PaymentSchedulesDashboard: React.FC<PaymentSchedulesDashboardProps> = ({ currentUser, onError, onCardUpdateSuccess }) => {
|
||||
const { t } = useTranslation('logged');
|
||||
|
||||
// list of displayed payment schedules
|
||||
@ -66,7 +66,7 @@ const PaymentSchedulesDashboard: React.FC<PaymentSchedulesDashboardProps> = ({ c
|
||||
* after a successful card update, provide a success message to the end-user
|
||||
*/
|
||||
const handleCardUpdateSuccess = (): void => {
|
||||
onCardUpdateSuccess(t('app.logged.dashboard.payment_schedules.card_updated_success'));
|
||||
onCardUpdateSuccess(t('app.logged.dashboard.payment_schedules_dashboard.card_updated_success'));
|
||||
};
|
||||
|
||||
/**
|
||||
@ -85,7 +85,7 @@ const PaymentSchedulesDashboard: React.FC<PaymentSchedulesDashboardProps> = ({ c
|
||||
|
||||
return (
|
||||
<div className="payment-schedules-dashboard">
|
||||
{!hasSchedules() && <div>{t('app.logged.dashboard.payment_schedules.no_payment_schedules')}</div>}
|
||||
{!hasSchedules() && <div>{t('app.logged.dashboard.payment_schedules_dashboard.no_payment_schedules')}</div>}
|
||||
{hasSchedules() && <div className="schedules-list">
|
||||
<PaymentSchedulesTable paymentSchedules={paymentSchedules}
|
||||
showCustomer={false}
|
||||
@ -93,7 +93,7 @@ const PaymentSchedulesDashboard: React.FC<PaymentSchedulesDashboardProps> = ({ c
|
||||
operator={currentUser}
|
||||
onError={onError}
|
||||
onCardUpdateSuccess={handleCardUpdateSuccess} />
|
||||
{hasMoreSchedules() && <FabButton className="load-more" onClick={handleLoadMore}>{t('app.logged.dashboard.payment_schedules.load_more')}</FabButton>}
|
||||
{hasMoreSchedules() && <FabButton className="load-more" onClick={handleLoadMore}>{t('app.logged.dashboard.payment_schedules_dashboard.load_more')}</FabButton>}
|
||||
</div>}
|
||||
</div>
|
||||
);
|
||||
|
@ -25,7 +25,7 @@ const PAGE_SIZE = 20;
|
||||
/**
|
||||
* This component shows a list of all payment schedules with their associated deadlines (aka. PaymentScheduleItem) and invoices
|
||||
*/
|
||||
const PaymentSchedulesList: React.FC<PaymentSchedulesListProps> = ({ currentUser, onError, onCardUpdateSuccess }) => {
|
||||
export const PaymentSchedulesList: React.FC<PaymentSchedulesListProps> = ({ currentUser, onError, onCardUpdateSuccess }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
// list of displayed payment schedules
|
||||
@ -93,19 +93,19 @@ const PaymentSchedulesList: React.FC<PaymentSchedulesListProps> = ({ currentUser
|
||||
* after a successful card update, provide a success message to the operator
|
||||
*/
|
||||
const handleCardUpdateSuccess = (): void => {
|
||||
onCardUpdateSuccess(t('app.admin.invoices.payment_schedules.card_updated_success'));
|
||||
onCardUpdateSuccess(t('app.admin.invoices.payment_schedules_list.card_updated_success'));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="payment-schedules-list">
|
||||
<h3>
|
||||
<i className="fas fa-filter" />
|
||||
{t('app.admin.invoices.payment_schedules.filter_schedules')}
|
||||
{t('app.admin.invoices.payment_schedules_list.filter_schedules')}
|
||||
</h3>
|
||||
<div className="schedules-filters">
|
||||
<DocumentFilters onFilterChange={handleFiltersChange} />
|
||||
</div>
|
||||
{!hasSchedules() && <div>{t('app.admin.invoices.payment_schedules.no_payment_schedules')}</div>}
|
||||
{!hasSchedules() && <div>{t('app.admin.invoices.payment_schedules_list.no_payment_schedules')}</div>}
|
||||
{hasSchedules() && <div className="schedules-list">
|
||||
<PaymentSchedulesTable paymentSchedules={paymentSchedules}
|
||||
showCustomer={true}
|
||||
@ -113,7 +113,7 @@ const PaymentSchedulesList: React.FC<PaymentSchedulesListProps> = ({ currentUser
|
||||
operator={currentUser}
|
||||
onError={onError}
|
||||
onCardUpdateSuccess={handleCardUpdateSuccess} />
|
||||
{hasMoreSchedules() && <FabButton className="load-more" onClick={handleLoadMore}>{t('app.admin.invoices.payment_schedules.load_more')}</FabButton>}
|
||||
{hasMoreSchedules() && <FabButton className="load-more" onClick={handleLoadMore}>{t('app.admin.invoices.payment_schedules_list.load_more')}</FabButton>}
|
||||
</div>}
|
||||
</div>
|
||||
);
|
||||
|
@ -26,7 +26,7 @@ interface PaymentSchedulesTableProps {
|
||||
/**
|
||||
* This component shows a list of all payment schedules with their associated deadlines (aka. PaymentScheduleItem) and invoices
|
||||
*/
|
||||
const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({ paymentSchedules, showCustomer, refreshList, operator, onError, onCardUpdateSuccess }) => {
|
||||
const PaymentSchedulesTable: React.FC<PaymentSchedulesTableProps> = ({ paymentSchedules, showCustomer, refreshList, operator, onError, onCardUpdateSuccess }) => {
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
// for each payment schedule: are the details (all deadlines) shown or hidden?
|
||||
@ -68,8 +68,10 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
*/
|
||||
const expandCollapseIcon = (paymentScheduleId: number): JSX.Element => {
|
||||
if (isExpanded(paymentScheduleId)) {
|
||||
// eslint-disable-next-line fabmanager/component-class-named-as-component
|
||||
return <i className="fas fa-minus-square" />;
|
||||
} else {
|
||||
// eslint-disable-next-line fabmanager/component-class-named-as-component
|
||||
return <i className="fas fa-plus-square" />;
|
||||
}
|
||||
};
|
||||
@ -93,9 +95,10 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
const downloadScheduleButton = (id: number): JSX.Element => {
|
||||
const link = `api/payment_schedules/${id}/download`;
|
||||
return (
|
||||
// eslint-disable-next-line fabmanager/component-class-named-as-component
|
||||
<a href={link} target="_blank" className="download-button" rel="noreferrer">
|
||||
<i className="fas fa-download" />
|
||||
{t('app.shared.schedules_table.download')}
|
||||
{t('app.shared.payment_schedules_table.download')}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
@ -104,11 +107,12 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
* Return the human-readable string for the status of the provided deadline.
|
||||
*/
|
||||
const formatState = (item: PaymentScheduleItem, schedule: PaymentSchedule): JSX.Element => {
|
||||
let res = t(`app.shared.schedules_table.state_${item.state}${item.state === 'pending' ? '_' + schedule.payment_method : ''}`);
|
||||
let res = t(`app.shared.payment_schedules_table.state_${item.state}${item.state === 'pending' ? '_' + schedule.payment_method : ''}`);
|
||||
if (item.state === PaymentScheduleItemState.Paid) {
|
||||
const key = `app.shared.schedules_table.method_${item.payment_method}`;
|
||||
res += ` (${t(key)})`;
|
||||
}
|
||||
// eslint-disable-next-line fabmanager/component-class-named-as-component
|
||||
return <span className={`state-${item.state}`}>{res}</span>;
|
||||
};
|
||||
|
||||
@ -119,16 +123,19 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
refreshList();
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the JSX table element that list all payment schedules and allows to perform actions on them.
|
||||
*/
|
||||
const renderPaymentSchedulesTable = (): ReactElement => {
|
||||
return (
|
||||
<table className="schedules-table">
|
||||
<table className="payment-schedules-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="w-35" />
|
||||
<th className="w-200">{t('app.shared.schedules_table.schedule_num')}</th>
|
||||
<th className="w-200">{t('app.shared.schedules_table.date')}</th>
|
||||
<th className="w-120">{t('app.shared.schedules_table.price')}</th>
|
||||
{showCustomer && <th className="w-200">{t('app.shared.schedules_table.customer')}</th>}
|
||||
<th className="w-200">{t('app.shared.payment_schedules_table.schedule_num')}</th>
|
||||
<th className="w-200">{t('app.shared.payment_schedules_table.date')}</th>
|
||||
<th className="w-120">{t('app.shared.payment_schedules_table.price')}</th>
|
||||
{showCustomer && <th className="w-200">{t('app.shared.payment_schedules_table.customer')}</th>}
|
||||
<th className="w-200"/>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -152,9 +159,9 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
<table className="schedule-items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="w-120">{t('app.shared.schedules_table.deadline')}</th>
|
||||
<th className="w-120">{t('app.shared.schedules_table.amount')}</th>
|
||||
<th className="w-200">{t('app.shared.schedules_table.state')}</th>
|
||||
<th className="w-120">{t('app.shared.payment_schedules_table.deadline')}</th>
|
||||
<th className="w-120">{t('app.shared.payment_schedules_table.amount')}</th>
|
||||
<th className="w-200">{t('app.shared.payment_schedules_table.state')}</th>
|
||||
<th className="w-200" />
|
||||
</tr>
|
||||
</thead>
|
||||
@ -212,12 +219,14 @@ const PaymentSchedulesTableComponent: React.FC<PaymentSchedulesTableProps> = ({
|
||||
return <div />;
|
||||
}
|
||||
};
|
||||
PaymentSchedulesTableComponent.defaultProps = { showCustomer: false };
|
||||
PaymentSchedulesTable.defaultProps = { showCustomer: false };
|
||||
|
||||
export const PaymentSchedulesTable: React.FC<PaymentSchedulesTableProps> = ({ paymentSchedules, showCustomer, refreshList, operator, onError, onCardUpdateSuccess }) => {
|
||||
const PaymentSchedulesTableWrapper: React.FC<PaymentSchedulesTableProps> = ({ paymentSchedules, showCustomer, refreshList, operator, onError, onCardUpdateSuccess }) => {
|
||||
return (
|
||||
<Loader>
|
||||
<PaymentSchedulesTableComponent paymentSchedules={paymentSchedules} showCustomer={showCustomer} refreshList={refreshList} operator={operator} onError={onError} onCardUpdateSuccess={onCardUpdateSuccess} />
|
||||
<PaymentSchedulesTable paymentSchedules={paymentSchedules} showCustomer={showCustomer} refreshList={refreshList} operator={operator} onError={onError} onCardUpdateSuccess={onCardUpdateSuccess} />
|
||||
</Loader>
|
||||
);
|
||||
};
|
||||
|
||||
export { PaymentSchedulesTableWrapper as PaymentSchedulesTable };
|
||||
|
@ -25,7 +25,7 @@ export const SelectSchedule: React.FC<SelectScheduleProps> = ({ show, selected,
|
||||
return (
|
||||
<div className="select-schedule">
|
||||
{show && <div className={className || ''}>
|
||||
<label htmlFor="payment_schedule">{ t('app.shared.cart.monthly_payment') }</label>
|
||||
<label htmlFor="payment_schedule">{ t('app.shared.select_schedule.monthly_payment') }</label>
|
||||
<Switch checked={selected} id="payment_schedule" onChange={onChange} className="schedule-switch" />
|
||||
</div>}
|
||||
</div>
|
||||
|
@ -19,6 +19,9 @@ interface UpdatePaymentMeanModalProps {
|
||||
*/
|
||||
type selectOption = { value: PaymentMethod, label: string };
|
||||
|
||||
/**
|
||||
* Component to allow the member to change his payment mean for the given payment schedule (e.g. from card to transfer)
|
||||
*/
|
||||
export const UpdatePaymentMeanModal: React.FC<UpdatePaymentMeanModalProps> = ({ isOpen, toggleModal, onError, afterSuccess, paymentSchedule }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { FunctionComponent, ReactNode, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import WalletLib from '../../lib/wallet';
|
||||
import { WalletInfo } from '../wallet-info';
|
||||
import { WalletInfo } from './wallet-info';
|
||||
import { FabModal, ModalSize } from '../base/fab-modal';
|
||||
import { HtmlTranslate } from '../base/html-translate';
|
||||
import { CustomAsset, CustomAssetName } from '../../models/custom-asset';
|
||||
@ -220,13 +220,13 @@ export const AbstractPaymentModal: React.FC<AbstractPaymentModalProps> = ({ isOp
|
||||
{errors}
|
||||
</div>}
|
||||
{hasPaymentScheduleInfo() && <div className="payment-schedule-info">
|
||||
<HtmlTranslate trKey="app.shared.payment.payment_schedule_html" options={{ DEADLINES: `${schedule.items.length}`, GATEWAY: gateway }} />
|
||||
<HtmlTranslate trKey="app.shared.abstract_payment_modal.payment_schedule_html" options={{ DEADLINES: `${schedule.items.length}`, GATEWAY: gateway }} />
|
||||
</div>}
|
||||
{hasCgv() && <div className="terms-of-sales">
|
||||
<input type="checkbox" id="acceptToS" name="acceptCondition" checked={tos} onChange={toggleTos} required />
|
||||
<label htmlFor="acceptToS">{ t('app.shared.payment.i_have_read_and_accept_') }
|
||||
<label htmlFor="acceptToS">{ t('app.shared.abstract_payment_modal.i_have_read_and_accept_') }
|
||||
<a href={cgv.custom_asset_file_attributes.attachment_url} target="_blank" rel="noreferrer">
|
||||
{ t('app.shared.payment._the_general_terms_and_conditions') }
|
||||
{ t('app.shared.abstract_payment_modal._the_general_terms_and_conditions') }
|
||||
</a>
|
||||
</label>
|
||||
</div>}
|
||||
@ -235,8 +235,8 @@ export const AbstractPaymentModal: React.FC<AbstractPaymentModalProps> = ({ isOp
|
||||
disabled={!canSubmit()}
|
||||
form={formId}
|
||||
className="validate-btn">
|
||||
{remainingPrice > 0 && t('app.shared.payment.confirm_payment_of_', { AMOUNT: FormatLib.price(remainingPrice) })}
|
||||
{remainingPrice === 0 && t('app.shared.payment.validate')}
|
||||
{remainingPrice > 0 && t('app.shared.abstract_payment_modal.confirm_payment_of_', { AMOUNT: FormatLib.price(remainingPrice) })}
|
||||
{remainingPrice === 0 && t('app.shared.abstract_payment_modal.validate')}
|
||||
</button>}
|
||||
{submitState && <div className="payment-pending">
|
||||
<div className="fa-2x">
|
||||
|
@ -2,7 +2,7 @@ import React, { ReactElement, useEffect, useState } from 'react';
|
||||
import { react2angular } from 'react2angular';
|
||||
import { Loader } from '../base/loader';
|
||||
import { StripeModal } from './stripe/stripe-modal';
|
||||
import { PayZenModal } from './payzen/payzen-modal';
|
||||
import { PayzenModal } from './payzen/payzen-modal';
|
||||
import { IApplication } from '../../models/application';
|
||||
import { ShoppingCart } from '../../models/payment';
|
||||
import { User } from '../../models/user';
|
||||
@ -29,7 +29,7 @@ interface CardPaymentModalProps {
|
||||
* This component open a modal dialog for the configured payment gateway, allowing the user to input his card data
|
||||
* to process an online payment.
|
||||
*/
|
||||
const CardPaymentModalComponent: React.FC<CardPaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, currentUser, schedule, cart, customer }) => {
|
||||
const CardPaymentModal: React.FC<CardPaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, currentUser, schedule, cart, customer }) => {
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
const [gateway, setGateway] = useState<Setting>(null);
|
||||
@ -58,7 +58,7 @@ const CardPaymentModalComponent: React.FC<CardPaymentModalProps> = ({ isOpen, to
|
||||
* Render the PayZen payment modal
|
||||
*/
|
||||
const renderPayZenModal = (): ReactElement => {
|
||||
return <PayZenModal isOpen={isOpen}
|
||||
return <PayzenModal isOpen={isOpen}
|
||||
toggleModal={toggleModal}
|
||||
afterSuccess={afterSuccess}
|
||||
onError={onError}
|
||||
@ -80,21 +80,23 @@ const CardPaymentModalComponent: React.FC<CardPaymentModalProps> = ({ isOpen, to
|
||||
return renderPayZenModal();
|
||||
case null:
|
||||
case undefined:
|
||||
onError(t('app.shared.payment_modal.online_payment_disabled'));
|
||||
onError(t('app.shared.card_payment_modal.online_payment_disabled'));
|
||||
return <div />;
|
||||
default:
|
||||
onError(t('app.shared.payment_modal.unexpected_error'));
|
||||
onError(t('app.shared.card_payment_modal.unexpected_error'));
|
||||
console.error(`[PaymentModal] Unimplemented gateway: ${gateway.value}`);
|
||||
return <div />;
|
||||
}
|
||||
};
|
||||
|
||||
export const CardPaymentModal: React.FC<CardPaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, currentUser, schedule, cart, customer }) => {
|
||||
const CardPaymentModalWrapper: React.FC<CardPaymentModalProps> = (props) => {
|
||||
return (
|
||||
<Loader>
|
||||
<CardPaymentModalComponent isOpen={isOpen} toggleModal={toggleModal} afterSuccess={afterSuccess} onError={onError} currentUser={currentUser} schedule={schedule} cart={cart} customer={customer} />
|
||||
<CardPaymentModal {...props} />
|
||||
</Loader>
|
||||
);
|
||||
};
|
||||
|
||||
Application.Components.component('cardPaymentModal', react2angular(CardPaymentModal, ['isOpen', 'toggleModal', 'afterSuccess', 'onError', 'currentUser', 'schedule', 'cart', 'customer']));
|
||||
export { CardPaymentModalWrapper as CardPaymentModal };
|
||||
|
||||
Application.Components.component('cardPaymentModal', react2angular(CardPaymentModalWrapper, ['isOpen', 'toggleModal', 'afterSuccess', 'onError', 'currentUser', 'schedule', 'cart', 'customer']));
|
||||
|
@ -54,7 +54,7 @@ export const LocalPaymentForm: React.FC<GatewayFormProps> = ({ onSubmit, onSucce
|
||||
const methodToOption = (value: scheduleMethod): selectOption => {
|
||||
if (!value) return { value, label: '' };
|
||||
|
||||
return { value, label: t(`app.admin.local_payment.method_${value}`) };
|
||||
return { value, label: t(`app.admin.local_payment_form.method_${value}`) };
|
||||
};
|
||||
|
||||
/**
|
||||
@ -77,7 +77,7 @@ export const LocalPaymentForm: React.FC<GatewayFormProps> = ({ onSubmit, onSucce
|
||||
try {
|
||||
const online = await SettingAPI.get(SettingName.OnlinePaymentModule);
|
||||
if (online.value !== 'true') {
|
||||
return onError(t('app.admin.local_payment.online_payment_disabled'));
|
||||
return onError(t('app.admin.local_payment_form.online_payment_disabled'));
|
||||
}
|
||||
return toggleOnlinePaymentModal();
|
||||
} catch (e) {
|
||||
@ -118,21 +118,21 @@ export const LocalPaymentForm: React.FC<GatewayFormProps> = ({ onSubmit, onSucce
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} id={formId} className={className || ''}>
|
||||
{!paymentSchedule && !isFreeOfCharge() && <p className="payment">{t('app.admin.local_payment.about_to_cash')}</p>}
|
||||
{!paymentSchedule && isFreeOfCharge() && <p className="payment">{t('app.admin.local_payment.about_to_confirm', { ITEM: mainItemType() })}</p>}
|
||||
<form onSubmit={handleSubmit} id={formId} className={`local-payment-form ${className || ''}`}>
|
||||
{!paymentSchedule && !isFreeOfCharge() && <p className="payment">{t('app.admin.local_payment_form.about_to_cash')}</p>}
|
||||
{!paymentSchedule && isFreeOfCharge() && <p className="payment">{t('app.admin.local_payment_form.about_to_confirm', { ITEM: mainItemType() })}</p>}
|
||||
{paymentSchedule && <div className="payment-schedule">
|
||||
<div className="schedule-method">
|
||||
<label htmlFor="payment-method">{t('app.admin.local_payment.payment_method')}</label>
|
||||
<Select placeholder={ t('app.admin.local_payment.payment_method') }
|
||||
<label htmlFor="payment-method">{t('app.admin.local_payment_form.payment_method')}</label>
|
||||
<Select placeholder={ t('app.admin.local_payment_form.payment_method') }
|
||||
id="payment-method"
|
||||
className="method-select"
|
||||
onChange={handleUpdateMethod}
|
||||
options={buildMethodOptions()}
|
||||
value={methodToOption(method)} />
|
||||
{method === 'card' && <p>{t('app.admin.local_payment.card_collection_info')}</p>}
|
||||
{method === 'check' && <p>{t('app.admin.local_payment.check_collection_info', { DEADLINES: paymentSchedule.items.length })}</p>}
|
||||
{method === 'transfer' && <HtmlTranslate trKey="app.admin.local_payment.transfer_collection_info" options={{ DEADLINES: paymentSchedule.items.length }} />}
|
||||
{method === 'card' && <p>{t('app.admin.local_payment_form.card_collection_info')}</p>}
|
||||
{method === 'check' && <p>{t('app.admin.local_payment_form.check_collection_info', { DEADLINES: paymentSchedule.items.length })}</p>}
|
||||
{method === 'transfer' && <HtmlTranslate trKey="app.admin.local_payment_form.transfer_collection_info" options={{ DEADLINES: paymentSchedule.items.length }} />}
|
||||
</div>
|
||||
<div className="full-schedule">
|
||||
<ul>
|
||||
|
@ -28,7 +28,7 @@ interface LocalPaymentModalProps {
|
||||
/**
|
||||
* This component enables a privileged user to confirm a local payments.
|
||||
*/
|
||||
const LocalPaymentModalComponent: React.FC<LocalPaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, cart, updateCart, currentUser, schedule, customer }) => {
|
||||
const LocalPaymentModal: React.FC<LocalPaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, cart, updateCart, currentUser, schedule, customer }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
/**
|
||||
@ -76,7 +76,7 @@ const LocalPaymentModalComponent: React.FC<LocalPaymentModalProps> = ({ isOpen,
|
||||
isOpen={isOpen}
|
||||
toggleModal={toggleModal}
|
||||
logoFooter={logoFooter()}
|
||||
title={isFreeOfCharge() ? t('app.admin.local_payment.validate_cart') : t('app.admin.local_payment.offline_payment')}
|
||||
title={isFreeOfCharge() ? t('app.admin.local_payment_modal.validate_cart') : t('app.admin.local_payment_modal.offline_payment')}
|
||||
formId="local-payment-form"
|
||||
formClassName="local-payment-form"
|
||||
currentUser={currentUser}
|
||||
@ -93,12 +93,14 @@ const LocalPaymentModalComponent: React.FC<LocalPaymentModalProps> = ({ isOpen,
|
||||
);
|
||||
};
|
||||
|
||||
export const LocalPaymentModal: React.FC<LocalPaymentModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, currentUser, schedule, cart, updateCart, customer }) => {
|
||||
const LocalPaymentModalWrapper: React.FC<LocalPaymentModalProps> = (props) => {
|
||||
return (
|
||||
<Loader>
|
||||
<LocalPaymentModalComponent isOpen={isOpen} toggleModal={toggleModal} afterSuccess={afterSuccess} onError={onError} currentUser={currentUser} schedule={schedule} cart={cart} updateCart={updateCart} customer={customer} />
|
||||
<LocalPaymentModal {...props} />
|
||||
</Loader>
|
||||
);
|
||||
};
|
||||
|
||||
Application.Components.component('localPaymentModal', react2angular(LocalPaymentModal, ['isOpen', 'toggleModal', 'afterSuccess', 'onError', 'currentUser', 'schedule', 'cart', 'updateCart', 'customer']));
|
||||
export { LocalPaymentModalWrapper as LocalPaymentModal };
|
||||
|
||||
Application.Components.component('localPaymentModal', react2angular(LocalPaymentModalWrapper, ['isOpen', 'toggleModal', 'afterSuccess', 'onError', 'currentUser', 'schedule', 'cart', 'updateCart', 'customer']));
|
||||
|
@ -16,6 +16,9 @@ interface PayzenCardUpdateModalProps {
|
||||
operator: User
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal dialog to allow the member to update his payment card for a payment schedule, when the PayZen gateway is used
|
||||
*/
|
||||
export const PayzenCardUpdateModal: React.FC<PayzenCardUpdateModalProps> = ({ isOpen, toggleModal, onSuccess, schedule, operator }) => {
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
@ -61,7 +64,7 @@ export const PayzenCardUpdateModal: React.FC<PayzenCardUpdateModalProps> = ({ is
|
||||
toggleModal={toggleModal}
|
||||
closeButton={false}
|
||||
customFooter={logoFooter()}
|
||||
className="payzen-update-card-modal">
|
||||
className="payzen-card-update-modal">
|
||||
{schedule && <PayzenForm onSubmit={handleCardUpdateSubmit}
|
||||
onSuccess={onSuccess}
|
||||
onError={handleCardUpdateError}
|
||||
|
@ -143,6 +143,9 @@ export const PayzenForm: React.FC<PayzenFormProps> = ({ onSubmit, onSuccess, onE
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a loader
|
||||
*/
|
||||
const Loader: FunctionComponent = () => {
|
||||
return (
|
||||
<div className={`fa-3x ${loadingClass}`}>
|
||||
@ -152,9 +155,9 @@ export const PayzenForm: React.FC<PayzenFormProps> = ({ onSubmit, onSuccess, onE
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} id={formId} className={className || ''}>
|
||||
<form onSubmit={handleSubmit} id={formId} className={`payzen-form ${className || ''}`}>
|
||||
<Loader />
|
||||
<div className="container">
|
||||
<div className="payzen-container">
|
||||
<div id="payzenPaymentForm" />
|
||||
</div>
|
||||
{children}
|
||||
|
@ -11,7 +11,7 @@ import PayzenAPI from '../../../api/payzen';
|
||||
|
||||
enableMapSet();
|
||||
|
||||
interface PayZenKeysFormProps {
|
||||
interface PayzenKeysFormProps {
|
||||
onValidKeys: (payZenSettings: Map<SettingName, string>) => void,
|
||||
onInvalidKeys: () => void,
|
||||
}
|
||||
@ -28,7 +28,7 @@ let pendingKeysValidation = false;
|
||||
/**
|
||||
* Form to set the PayZen's username, password and public key
|
||||
*/
|
||||
const PayZenKeysFormComponent: React.FC<PayZenKeysFormProps> = ({ onValidKeys, onInvalidKeys }) => {
|
||||
const PayzenKeysForm: React.FC<PayzenKeysFormProps> = ({ onValidKeys, onInvalidKeys }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
// values of the PayZen settings
|
||||
@ -139,13 +139,13 @@ const PayZenKeysFormComponent: React.FC<PayZenKeysFormProps> = ({ onValidKeys, o
|
||||
return (
|
||||
<div className="payzen-keys-form">
|
||||
<div className="payzen-keys-info">
|
||||
<HtmlTranslate trKey="app.admin.invoices.payment.payzen_keys_info_html" />
|
||||
<HtmlTranslate trKey="app.admin.invoices.payzen_keys_form.payzen_keys_info_html" />
|
||||
</div>
|
||||
<form name="payzenKeysForm">
|
||||
<fieldset>
|
||||
<legend>{t('app.admin.invoices.payment.client_keys')}</legend>
|
||||
<legend>{t('app.admin.invoices.payzen_keys_form.client_keys')}</legend>
|
||||
<div className="payzen-public-input">
|
||||
<label htmlFor="payzen_public_key">{ t('app.admin.invoices.payment.payzen.payzen_public_key') } *</label>
|
||||
<label htmlFor="payzen_public_key">{ t('app.admin.invoices.payzen_keys_form.payzen_public_key') } *</label>
|
||||
<FabInput id="payzen_public_key"
|
||||
icon={<i className="fas fa-info" />}
|
||||
defaultValue={settings.get(SettingName.PayZenPublicKey)}
|
||||
@ -158,11 +158,11 @@ const PayZenKeysFormComponent: React.FC<PayZenKeysFormProps> = ({ onValidKeys, o
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend className={hasApiAddOn() ? 'with-addon' : ''}>
|
||||
<span>{t('app.admin.invoices.payment.api_keys')}</span>
|
||||
<span>{t('app.admin.invoices.payzen_keys_form.api_keys')}</span>
|
||||
{hasApiAddOn() && <span className={`fieldset-legend--addon ${restApiAddOnClassName || ''}`}>{restApiAddOn}</span>}
|
||||
</legend>
|
||||
<div className="payzen-api-user-input">
|
||||
<label htmlFor="payzen_username">{ t('app.admin.invoices.payment.payzen.payzen_username') } *</label>
|
||||
<label htmlFor="payzen_username">{ t('app.admin.invoices.payzen_keys_form.payzen_username') } *</label>
|
||||
<FabInput id="payzen_username"
|
||||
type="number"
|
||||
icon={<i className="fas fa-user-alt" />}
|
||||
@ -172,7 +172,7 @@ const PayZenKeysFormComponent: React.FC<PayZenKeysFormProps> = ({ onValidKeys, o
|
||||
required />
|
||||
</div>
|
||||
<div className="payzen-api-password-input">
|
||||
<label htmlFor="payzen_password">{ t('app.admin.invoices.payment.payzen.payzen_password') } *</label>
|
||||
<label htmlFor="payzen_password">{ t('app.admin.invoices.payzen_keys_form.payzen_password') } *</label>
|
||||
<FabInput id="payzen_password"
|
||||
icon={<i className="fas fa-key" />}
|
||||
defaultValue={settings.get(SettingName.PayZenPassword)}
|
||||
@ -181,7 +181,7 @@ const PayZenKeysFormComponent: React.FC<PayZenKeysFormProps> = ({ onValidKeys, o
|
||||
required />
|
||||
</div>
|
||||
<div className="payzen-api-endpoint-input">
|
||||
<label htmlFor="payzen_endpoint">{ t('app.admin.invoices.payment.payzen.payzen_endpoint') } *</label>
|
||||
<label htmlFor="payzen_endpoint">{ t('app.admin.invoices.payzen_keys_form.payzen_endpoint') } *</label>
|
||||
<FabInput id="payzen_endpoint"
|
||||
type="url"
|
||||
icon={<i className="fas fa-link" />}
|
||||
@ -191,7 +191,7 @@ const PayZenKeysFormComponent: React.FC<PayZenKeysFormProps> = ({ onValidKeys, o
|
||||
required />
|
||||
</div>
|
||||
<div className="payzen-api-hmac-input">
|
||||
<label htmlFor="payzen_hmac">{ t('app.admin.invoices.payment.payzen.payzen_hmac') } *</label>
|
||||
<label htmlFor="payzen_hmac">{ t('app.admin.invoices.payzen_keys_form.payzen_hmac') } *</label>
|
||||
<FabInput id="payzen_hmac"
|
||||
icon={<i className="fas fa-subscript" />}
|
||||
defaultValue={settings.get(SettingName.PayZenHmacKey)}
|
||||
@ -205,10 +205,12 @@ const PayZenKeysFormComponent: React.FC<PayZenKeysFormProps> = ({ onValidKeys, o
|
||||
);
|
||||
};
|
||||
|
||||
export const PayZenKeysForm: React.FC<PayZenKeysFormProps> = ({ onValidKeys, onInvalidKeys }) => {
|
||||
const PayzenKeysFormWrapper: React.FC<PayzenKeysFormProps> = (props) => {
|
||||
return (
|
||||
<Loader>
|
||||
<PayZenKeysFormComponent onValidKeys={onValidKeys} onInvalidKeys={onInvalidKeys} />
|
||||
<PayzenKeysForm {...props} />
|
||||
</Loader>
|
||||
);
|
||||
};
|
||||
|
||||
export { PayzenKeysFormWrapper as PayzenKeysForm };
|
||||
|
@ -10,7 +10,7 @@ import mastercardLogo from '../../../../../images/mastercard.png';
|
||||
import visaLogo from '../../../../../images/visa.png';
|
||||
import { PayzenForm } from './payzen-form';
|
||||
|
||||
interface PayZenModalProps {
|
||||
interface PayzenModalProps {
|
||||
isOpen: boolean,
|
||||
toggleModal: () => void,
|
||||
afterSuccess: (result: Invoice|PaymentSchedule) => void,
|
||||
@ -28,7 +28,7 @@ interface PayZenModalProps {
|
||||
* This component should not be called directly. Prefer using <CardPaymentModal> which can handle the configuration
|
||||
* of a different payment gateway.
|
||||
*/
|
||||
export const PayZenModal: React.FC<PayZenModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, cart, currentUser, schedule, customer }) => {
|
||||
export const PayzenModal: React.FC<PayzenModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, cart, currentUser, schedule, customer }) => {
|
||||
/**
|
||||
* Return the logos, shown in the modal footer.
|
||||
*/
|
||||
|
@ -83,7 +83,7 @@ export const PayzenSettings: React.FC<PayzenSettingsProps> = ({ onEditKeys, onCu
|
||||
setError('');
|
||||
updateSettings(draft => draft.set(SettingName.PayZenCurrency, value));
|
||||
} else {
|
||||
setError(t('app.admin.invoices.payment.payzen.currency_error'));
|
||||
setError(t('app.admin.invoices.payment.payzen_settings.currency_error'));
|
||||
}
|
||||
};
|
||||
|
||||
@ -97,18 +97,18 @@ export const PayzenSettings: React.FC<PayzenSettingsProps> = ({ onEditKeys, onCu
|
||||
updateSettings(draft => draft.set(SettingName.PayZenCurrency, result.value));
|
||||
onCurrencyUpdateSuccess(result.value);
|
||||
}, reason => {
|
||||
setError(t('app.admin.invoices.payment.payzen.error_while_saving') + reason);
|
||||
setError(t('app.admin.invoices.payment.payzen_settings.error_while_saving') + reason);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="payzen-settings">
|
||||
<h3 className="title">{t('app.admin.invoices.payment.payzen.payzen_keys')}</h3>
|
||||
<h3 className="title">{t('app.admin.invoices.payment.payzen_settings.payzen_keys')}</h3>
|
||||
<div className="payzen-keys">
|
||||
{payZenPublicSettings.concat(payZenPrivateSettings).map(setting => {
|
||||
return (
|
||||
<div className="key-wrapper" key={setting}>
|
||||
<label htmlFor={setting}>{t(`app.admin.invoices.payment.payzen.${setting}`)}</label>
|
||||
<label htmlFor={setting}>{t(`app.admin.invoices.payment.payzen_settings.${setting}`)}</label>
|
||||
<FabInput defaultValue={settings.get(setting)}
|
||||
id={setting}
|
||||
type={payZenPrivateSettings.indexOf(setting) > -1 ? 'password' : 'text'}
|
||||
@ -119,17 +119,17 @@ export const PayzenSettings: React.FC<PayzenSettingsProps> = ({ onEditKeys, onCu
|
||||
);
|
||||
})}
|
||||
<div className="edit-keys">
|
||||
<FabButton className="edit-keys-btn" onClick={handleKeysUpdate}>{t('app.admin.invoices.payment.edit_keys')}</FabButton>
|
||||
<FabButton className="edit-keys-btn" onClick={handleKeysUpdate}>{t('app.admin.invoices.payment.payzen_settings.edit_keys')}</FabButton>
|
||||
</div>
|
||||
</div>
|
||||
<div className="payzen-currency">
|
||||
<h3 className="title">{t('app.admin.invoices.payment.payzen.currency')}</h3>
|
||||
<h3 className="title">{t('app.admin.invoices.payment.payzen_settings.currency')}</h3>
|
||||
<p className="currency-info">
|
||||
<HtmlTranslate trKey="app.admin.invoices.payment.payzen.currency_info_html" />
|
||||
<HtmlTranslate trKey="app.admin.invoices.payment.payzen_settings.currency_info_html" />
|
||||
</p>
|
||||
<div className="payzen-currency-form">
|
||||
<div className="currency-wrapper">
|
||||
<label htmlFor="payzen_currency">{t('app.admin.invoices.payment.payzen.payzen_currency')}</label>
|
||||
<label htmlFor="payzen_currency">{t('app.admin.invoices.payment.payzen_settings.payzen_currency')}</label>
|
||||
<FabInput defaultValue={settings.get(SettingName.PayZenCurrency)}
|
||||
id="payzen_currency"
|
||||
icon={<i className="fas fa-money-bill" />}
|
||||
@ -138,7 +138,7 @@ export const PayzenSettings: React.FC<PayzenSettingsProps> = ({ onEditKeys, onCu
|
||||
pattern="[A-Z]{3}"
|
||||
error={error} />
|
||||
</div>
|
||||
<FabButton className="save-currency" onClick={saveCurrency}>{t('app.admin.invoices.payment.payzen.save')}</FabButton>
|
||||
<FabButton className="save-currency" onClick={saveCurrency}>{t('app.admin.invoices.payment.payzen_settings.save')}</FabButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -6,15 +6,15 @@
|
||||
import React, { BaseSyntheticEvent, useEffect, useState } from 'react';
|
||||
import { react2angular } from 'react2angular';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StripeKeysForm } from './payment/stripe/stripe-keys-form';
|
||||
import { PayZenKeysForm } from './payment/payzen/payzen-keys-form';
|
||||
import { FabModal, ModalSize } from './base/fab-modal';
|
||||
import { Loader } from './base/loader';
|
||||
import { User } from '../models/user';
|
||||
import { Gateway } from '../models/gateway';
|
||||
import { SettingBulkResult, SettingName } from '../models/setting';
|
||||
import { IApplication } from '../models/application';
|
||||
import SettingAPI from '../api/setting';
|
||||
import { StripeKeysForm } from './stripe/stripe-keys-form';
|
||||
import { PayzenKeysForm } from './payzen/payzen-keys-form';
|
||||
import { FabModal, ModalSize } from '../base/fab-modal';
|
||||
import { Loader } from '../base/loader';
|
||||
import { User } from '../../models/user';
|
||||
import { Gateway } from '../../models/gateway';
|
||||
import { SettingBulkResult, SettingName } from '../../models/setting';
|
||||
import { IApplication } from '../../models/application';
|
||||
import SettingAPI from '../../api/setting';
|
||||
|
||||
declare const Application: IApplication;
|
||||
|
||||
@ -26,7 +26,10 @@ interface SelectGatewayModalModalProps {
|
||||
onSuccess: (results: Map<SettingName, SettingBulkResult>) => void,
|
||||
}
|
||||
|
||||
const SelectGatewayModal: React.FC<SelectGatewayModalModalProps> = ({ isOpen, toggleModal, onError, onSuccess }) => {
|
||||
/**
|
||||
* Modal dialog that enable an admin to configure the active payment gateway
|
||||
*/
|
||||
export const SelectGatewayModal: React.FC<SelectGatewayModalModalProps> = ({ isOpen, toggleModal, onError, onSuccess }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
const [preventConfirmGateway, setPreventConfirmGateway] = useState<boolean>(true);
|
||||
@ -113,33 +116,33 @@ const SelectGatewayModal: React.FC<SelectGatewayModalModalProps> = ({ isOpen, to
|
||||
};
|
||||
|
||||
return (
|
||||
<FabModal title={t('app.admin.invoices.payment.gateway_modal.select_gateway_title')}
|
||||
<FabModal title={t('app.admin.invoices.payment.select_gateway_modal.select_gateway_title')}
|
||||
isOpen={isOpen}
|
||||
toggleModal={toggleModal}
|
||||
width={ModalSize.medium}
|
||||
className="gateway-modal"
|
||||
confirmButton={t('app.admin.invoices.payment.gateway_modal.confirm_button')}
|
||||
className="select-gateway-modal"
|
||||
confirmButton={t('app.admin.invoices.payment.select_gateway_modal.confirm_button')}
|
||||
onConfirm={onGatewayConfirmed}
|
||||
preventConfirm={preventConfirmGateway}>
|
||||
{!hasSelectedGateway() && <p className="info-gateway">
|
||||
{t('app.admin.invoices.payment.gateway_modal.gateway_info')}
|
||||
{t('app.admin.invoices.payment.select_gateway_modal.gateway_info')}
|
||||
</p>}
|
||||
<label htmlFor="gateway">{t('app.admin.invoices.payment.gateway_modal.select_gateway')}</label>
|
||||
<label htmlFor="gateway">{t('app.admin.invoices.payment.select_gateway_modal.select_gateway')}</label>
|
||||
<select id="gateway" className="select-gateway" onChange={setGateway} value={selectedGateway}>
|
||||
<option />
|
||||
<option value={Gateway.Stripe}>{t('app.admin.invoices.payment.gateway_modal.stripe')}</option>
|
||||
<option value={Gateway.PayZen}>{t('app.admin.invoices.payment.gateway_modal.payzen')}</option>
|
||||
<option value={Gateway.Stripe}>{t('app.admin.invoices.payment.select_gateway_modal.stripe')}</option>
|
||||
<option value={Gateway.PayZen}>{t('app.admin.invoices.payment.select_gateway_modal.payzen')}</option>
|
||||
</select>
|
||||
{selectedGateway === Gateway.Stripe && <StripeKeysForm onValidKeys={handleValidStripeKeys} onInvalidKeys={handleInvalidKeys} />}
|
||||
{selectedGateway === Gateway.PayZen && <PayZenKeysForm onValidKeys={handleValidPayZenKeys} onInvalidKeys={handleInvalidKeys} />}
|
||||
{selectedGateway === Gateway.PayZen && <PayzenKeysForm onValidKeys={handleValidPayZenKeys} onInvalidKeys={handleInvalidKeys} />}
|
||||
</FabModal>
|
||||
);
|
||||
};
|
||||
|
||||
const SelectGatewayModalWrapper: React.FC<SelectGatewayModalModalProps> = ({ isOpen, toggleModal, currentUser, onSuccess, onError }) => {
|
||||
const SelectGatewayModalWrapper: React.FC<SelectGatewayModalModalProps> = (props) => {
|
||||
return (
|
||||
<Loader>
|
||||
<SelectGatewayModal isOpen={isOpen} toggleModal={toggleModal} currentUser={currentUser} onSuccess={onSuccess} onError={onError} />
|
||||
<SelectGatewayModal {...props} />
|
||||
</Loader>
|
||||
);
|
||||
};
|
@ -16,6 +16,9 @@ interface StripeCardUpdateModalProps {
|
||||
operator: User
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal dialog to allow the member to update his payment card for a payment schedule, when the Stripe gateway is used
|
||||
*/
|
||||
export const StripeCardUpdateModal: React.FC<StripeCardUpdateModalProps> = ({ isOpen, toggleModal, onSuccess, schedule, operator }) => {
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
@ -30,7 +33,7 @@ export const StripeCardUpdateModal: React.FC<StripeCardUpdateModalProps> = ({ is
|
||||
const logoFooter = (): ReactNode => {
|
||||
return (
|
||||
<div className="stripe-modal-icons">
|
||||
<i className="fa fa-lock fa-2x m-r-sm pos-rlt" />
|
||||
<i className="fa fa-lock fa-2x" />
|
||||
<img src={stripeLogo} alt="powered by stripe" />
|
||||
<img src={mastercardLogo} alt="mastercard" />
|
||||
<img src={visaLogo} alt="visa" />
|
||||
@ -59,7 +62,7 @@ export const StripeCardUpdateModal: React.FC<StripeCardUpdateModalProps> = ({ is
|
||||
toggleModal={toggleModal}
|
||||
closeButton={false}
|
||||
customFooter={logoFooter()}
|
||||
className="stripe-update-card-modal">
|
||||
className="stripe-card-update-modal">
|
||||
{schedule && <StripeCardUpdate onSubmit={handleCardUpdateSubmit}
|
||||
onSuccess={onSuccess}
|
||||
onError={handleCardUpdateError}
|
||||
|
@ -95,7 +95,7 @@ export const StripeCardUpdate: React.FC<StripeCardUpdateProps> = ({ onSubmit, on
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} id="stripe-card" className={className}>
|
||||
<form onSubmit={handleSubmit} id="stripe-card" className={`stripe-card-update ${className}`}>
|
||||
<CardElement options={cardOptions} />
|
||||
{children}
|
||||
</form>
|
||||
|
@ -43,11 +43,11 @@ export const StripeConfirmModal: React.FC<StripeConfirmModalProps> = ({ isOpen,
|
||||
};
|
||||
|
||||
return (
|
||||
<FabModal title={t('app.shared.schedules_table.resolve_action')}
|
||||
<FabModal title={t('app.shared.stripe_confirm_modal.resolve_action')}
|
||||
isOpen={isOpen}
|
||||
toggleModal={toggleModal}
|
||||
onConfirm={onConfirmed}
|
||||
confirmButton={t('app.shared.schedules_table.ok_button')}
|
||||
confirmButton={t('app.shared.stripe_confirm_modal.ok_button')}
|
||||
preventConfirm={isPending}>
|
||||
{item && <StripeConfirm clientSecret={item.client_secret} onResponse={togglePending} />}
|
||||
</FabModal>
|
||||
|
@ -66,7 +66,7 @@ export const StripeForm: React.FC<GatewayFormProps> = ({ onSubmit, onSuccess, on
|
||||
if (response.error.statusText) {
|
||||
onError(response.error.statusText);
|
||||
} else {
|
||||
onError(`${t('app.shared.messages.payment_card_error')} ${response.error}`);
|
||||
onError(`${t('app.shared.stripe_form.payment_card_error')} ${response.error}`);
|
||||
}
|
||||
} else if ('requires_action' in response) {
|
||||
if (response.type === 'payment') {
|
||||
@ -125,7 +125,7 @@ export const StripeForm: React.FC<GatewayFormProps> = ({ onSubmit, onSuccess, on
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} id={formId} className={className || ''}>
|
||||
<form onSubmit={handleSubmit} id={formId} className={`stripe-form ${className || ''}`}>
|
||||
<CardElement options={cardOptions} />
|
||||
{children}
|
||||
</form>
|
||||
|
@ -15,7 +15,7 @@ interface StripeKeysFormProps {
|
||||
/**
|
||||
* Form to set the stripe's public and private keys
|
||||
*/
|
||||
const StripeKeysFormComponent: React.FC<StripeKeysFormProps> = ({ onValidKeys, onInvalidKeys }) => {
|
||||
const StripeKeysForm: React.FC<StripeKeysFormProps> = ({ onValidKeys, onInvalidKeys }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
// used to prevent promises from resolving if the component was unmounted
|
||||
@ -123,11 +123,11 @@ const StripeKeysFormComponent: React.FC<StripeKeysFormProps> = ({ onValidKeys, o
|
||||
return (
|
||||
<div className="stripe-keys-form">
|
||||
<div className="stripe-keys-info">
|
||||
<HtmlTranslate trKey="app.admin.invoices.payment.stripe_keys_info_html" />
|
||||
<HtmlTranslate trKey="app.admin.invoices.stripe_keys_form.stripe_keys_info_html" />
|
||||
</div>
|
||||
<form name="stripeKeysForm">
|
||||
<div className="stripe-public-input">
|
||||
<label htmlFor="stripe_public_key">{ t('app.admin.invoices.payment.public_key') } *</label>
|
||||
<label htmlFor="stripe_public_key">{ t('app.admin.invoices.stripe_keys_form.public_key') } *</label>
|
||||
<FabInput id="stripe_public_key"
|
||||
icon={<i className="fa fa-info" />}
|
||||
defaultValue={publicKey}
|
||||
@ -138,7 +138,7 @@ const StripeKeysFormComponent: React.FC<StripeKeysFormProps> = ({ onValidKeys, o
|
||||
required />
|
||||
</div>
|
||||
<div className="stripe-secret-input">
|
||||
<label htmlFor="stripe_secret_key">{ t('app.admin.invoices.payment.secret_key') } *</label>
|
||||
<label htmlFor="stripe_secret_key">{ t('app.admin.invoices.stripe_keys_form.secret_key') } *</label>
|
||||
<FabInput id="stripe_secret_key"
|
||||
icon={<i className="fa fa-key" />}
|
||||
defaultValue={secretKey}
|
||||
@ -153,10 +153,12 @@ const StripeKeysFormComponent: React.FC<StripeKeysFormProps> = ({ onValidKeys, o
|
||||
);
|
||||
};
|
||||
|
||||
export const StripeKeysForm: React.FC<StripeKeysFormProps> = ({ onValidKeys, onInvalidKeys }) => {
|
||||
const StripeKeysFormWrapper: React.FC<StripeKeysFormProps> = (props) => {
|
||||
return (
|
||||
<Loader>
|
||||
<StripeKeysFormComponent onValidKeys={onValidKeys} onInvalidKeys={onInvalidKeys} />
|
||||
<StripeKeysForm {...props} />
|
||||
</Loader>
|
||||
);
|
||||
};
|
||||
|
||||
export { StripeKeysFormWrapper as StripeKeysForm };
|
||||
|
@ -36,7 +36,7 @@ export const StripeModal: React.FC<StripeModalProps> = ({ isOpen, toggleModal, a
|
||||
const logoFooter = (): ReactNode => {
|
||||
return (
|
||||
<div className="stripe-modal-icons">
|
||||
<i className="fa fa-lock fa-2x m-r-sm pos-rlt" />
|
||||
<i className="fa fa-lock fa-2x" />
|
||||
<img src={stripeLogo} alt="powered by stripe" />
|
||||
<img src={mastercardLogo} alt="mastercard" />
|
||||
<img src={visaLogo} alt="visa" />
|
||||
|
@ -19,7 +19,7 @@ interface UpdateCardModalProps {
|
||||
* This component open a modal dialog for the configured payment gateway, allowing the user to input his card data
|
||||
* to process an online payment.
|
||||
*/
|
||||
const UpdateCardModalComponent: React.FC<UpdateCardModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, operator, schedule }) => {
|
||||
const UpdateCardModal: React.FC<UpdateCardModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, operator, schedule }) => {
|
||||
const { t } = useTranslation('shared');
|
||||
const [gateway, setGateway] = useState<string>('');
|
||||
|
||||
@ -68,10 +68,12 @@ const UpdateCardModalComponent: React.FC<UpdateCardModalProps> = ({ isOpen, togg
|
||||
}
|
||||
};
|
||||
|
||||
export const UpdateCardModal: React.FC<UpdateCardModalProps> = ({ isOpen, toggleModal, afterSuccess, onError, operator, schedule }) => {
|
||||
const UpdateCardModalWrapper: React.FC<UpdateCardModalProps> = (props) => {
|
||||
return (
|
||||
<Loader>
|
||||
<UpdateCardModalComponent isOpen={isOpen} toggleModal={toggleModal} afterSuccess={afterSuccess} onError={onError} operator={operator} schedule={schedule} />
|
||||
<UpdateCardModal {...props} />
|
||||
</Loader>
|
||||
);
|
||||
};
|
||||
|
||||
export { UpdateCardModalWrapper as UpdateCardModal };
|
||||
|
@ -1,14 +1,14 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { react2angular } from 'react2angular';
|
||||
import { IApplication } from '../models/application';
|
||||
import '../lib/i18n';
|
||||
import { Loader } from './base/loader';
|
||||
import { User } from '../models/user';
|
||||
import { Wallet } from '../models/wallet';
|
||||
import WalletLib from '../lib/wallet';
|
||||
import { ShoppingCart } from '../models/payment';
|
||||
import FormatLib from '../lib/format';
|
||||
import { IApplication } from '../../models/application';
|
||||
import '../../lib/i18n';
|
||||
import { Loader } from '../base/loader';
|
||||
import { User } from '../../models/user';
|
||||
import { Wallet } from '../../models/wallet';
|
||||
import WalletLib from '../../lib/wallet';
|
||||
import { ShoppingCart } from '../../models/payment';
|
||||
import FormatLib from '../../lib/format';
|
||||
|
||||
declare const Application: IApplication;
|
||||
|
@ -16,7 +16,7 @@ interface DeletePlanCategoryProps {
|
||||
* This component shows a button.
|
||||
* When clicked, we show a modal dialog to ask the user for confirmation about the deletion of the provided plan-category.
|
||||
*/
|
||||
const DeletePlanCategoryComponent: React.FC<DeletePlanCategoryProps> = ({ onSuccess, onError, category }) => {
|
||||
const DeletePlanCategory: React.FC<DeletePlanCategoryProps> = ({ onSuccess, onError, category }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
const [deletionModal, setDeletionModal] = useState<boolean>(false);
|
||||
@ -34,9 +34,9 @@ const DeletePlanCategoryComponent: React.FC<DeletePlanCategoryProps> = ({ onSucc
|
||||
*/
|
||||
const onDeleteConfirmed = (): void => {
|
||||
PlanCategoryAPI.destroy(category.id).then(() => {
|
||||
onSuccess(t('app.admin.manage_plan_category.delete_category.success'));
|
||||
onSuccess(t('app.admin.delete_plan_category.success'));
|
||||
}).catch((error) => {
|
||||
onError(t('app.admin.manage_plan_category.delete_category.error') + error);
|
||||
onError(t('app.admin.delete_plan_category.error') + error);
|
||||
});
|
||||
toggleDeletionModal();
|
||||
};
|
||||
@ -44,22 +44,24 @@ const DeletePlanCategoryComponent: React.FC<DeletePlanCategoryProps> = ({ onSucc
|
||||
return (
|
||||
<div className="delete-plan-category">
|
||||
<FabButton type='button' className="delete-button" icon={<i className="fa fa-trash" />} onClick={toggleDeletionModal} />
|
||||
<FabModal title={t('app.admin.manage_plan_category.delete_category.title')}
|
||||
<FabModal title={t('app.admin.delete_plan_category.title')}
|
||||
isOpen={deletionModal}
|
||||
toggleModal={toggleDeletionModal}
|
||||
closeButton={true}
|
||||
confirmButton={t('app.admin.manage_plan_category.delete_category.cta')}
|
||||
confirmButton={t('app.admin.delete_plan_category.cta')}
|
||||
onConfirm={onDeleteConfirmed}>
|
||||
<span>{t('app.admin.manage_plan_category.delete_category.confirm')}</span>
|
||||
<span>{t('app.admin.delete_plan_category.confirm')}</span>
|
||||
</FabModal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const DeletePlanCategory: React.FC<DeletePlanCategoryProps> = ({ onSuccess, onError, category }) => {
|
||||
const DeletePlanCategoryWrapper: React.FC<DeletePlanCategoryProps> = (props) => {
|
||||
return (
|
||||
<Loader>
|
||||
<DeletePlanCategoryComponent onSuccess={onSuccess} onError={onError} category={category} />
|
||||
<DeletePlanCategory {...props} />
|
||||
</Loader>
|
||||
);
|
||||
};
|
||||
|
||||
export { DeletePlanCategoryWrapper as DeletePlanCategory };
|
||||
|
@ -17,7 +17,7 @@ interface ManagePlanCategoryProps {
|
||||
* This component shows a button.
|
||||
* When clicked, we show a modal dialog allowing to fill the parameters of a plan-category (create new or update existing).
|
||||
*/
|
||||
const ManagePlanCategoryComponent: React.FC<ManagePlanCategoryProps> = ({ category, action, onSuccess, onError }) => {
|
||||
const ManagePlanCategory: React.FC<ManagePlanCategoryProps> = ({ category, action, onSuccess, onError }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
// is the creation modal open?
|
||||
@ -61,9 +61,9 @@ const ManagePlanCategoryComponent: React.FC<ManagePlanCategoryProps> = ({ catego
|
||||
return (
|
||||
<FabButton type='button'
|
||||
icon={<i className='fa fa-plus' />}
|
||||
className="btn-warning"
|
||||
className="create-button"
|
||||
onClick={toggleModal}>
|
||||
{t('app.admin.manage_plan_category.create_category.title')}
|
||||
{t('app.admin.manage_plan_category.create')}
|
||||
</FabButton>
|
||||
);
|
||||
case 'update':
|
||||
@ -77,7 +77,7 @@ const ManagePlanCategoryComponent: React.FC<ManagePlanCategoryProps> = ({ catego
|
||||
return (
|
||||
<div className='manage-plan-category'>
|
||||
{ toggleBtn() }
|
||||
<FabModal title={t(`app.admin.manage_plan_category.${action}_category.title`)}
|
||||
<FabModal title={t(`app.admin.manage_plan_category.${action}`)}
|
||||
isOpen={isOpen}
|
||||
toggleModal={toggleModal}
|
||||
onCreation={initCategoryCreation}
|
||||
@ -90,10 +90,12 @@ const ManagePlanCategoryComponent: React.FC<ManagePlanCategoryProps> = ({ catego
|
||||
);
|
||||
};
|
||||
|
||||
export const ManagePlanCategory: React.FC<ManagePlanCategoryProps> = ({ category, action, onSuccess, onError }) => {
|
||||
const ManagePlanCategoryWrapper: React.FC<ManagePlanCategoryProps> = (props) => {
|
||||
return (
|
||||
<Loader>
|
||||
<ManagePlanCategoryComponent category={category} action={action} onSuccess={onSuccess} onError={onError} />
|
||||
<ManagePlanCategory {...props} />
|
||||
</Loader>
|
||||
);
|
||||
};
|
||||
|
||||
export { ManagePlanCategoryWrapper as ManagePlanCategory };
|
||||
|
@ -16,7 +16,10 @@ interface PlanCategoryFormProps {
|
||||
onError: (message: string) => void
|
||||
}
|
||||
|
||||
const PlanCategoryFormComponent: React.FC<PlanCategoryFormProps> = ({ action, category, onSuccess, onError }) => {
|
||||
/**
|
||||
* Form to create/edit a plan category
|
||||
*/
|
||||
const PlanCategoryForm: React.FC<PlanCategoryFormProps> = ({ action, category, onSuccess, onError }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
const { register, control, handleSubmit } = useForm<PlanCategory>({ defaultValues: { ...category } });
|
||||
@ -28,16 +31,16 @@ const PlanCategoryFormComponent: React.FC<PlanCategoryFormProps> = ({ action, ca
|
||||
switch (action) {
|
||||
case 'create':
|
||||
PlanCategoryAPI.create(data).then(() => {
|
||||
onSuccess(t('app.admin.manage_plan_category.create_category.success'));
|
||||
onSuccess(t('app.admin.plan_category_form.create.success'));
|
||||
}).catch((error) => {
|
||||
onError(t('app.admin.manage_plan_category.create_category.error') + error);
|
||||
onError(t('app.admin.plan_category_form.create.error') + error);
|
||||
});
|
||||
break;
|
||||
case 'update':
|
||||
PlanCategoryAPI.update(data).then(() => {
|
||||
onSuccess(t('app.admin.manage_plan_category.update_category.success'));
|
||||
onSuccess(t('app.admin.plan_category_form.update.success'));
|
||||
}).catch((error) => {
|
||||
onError(t('app.admin.manage_plan_category.update_category.error') + error);
|
||||
onError(t('app.admin.plan_category_form.update.error') + error);
|
||||
});
|
||||
break;
|
||||
}
|
||||
@ -45,23 +48,25 @@ const PlanCategoryFormComponent: React.FC<PlanCategoryFormProps> = ({ action, ca
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<FormInput id='name' register={register} rules={{ required: 'true' }} label={t('app.admin.manage_plan_category.name')} />
|
||||
<FormInput id='name' register={register} rules={{ required: 'true' }} label={t('app.admin.plan_category_form.name')} />
|
||||
|
||||
<FormRichText control={control} id="description" label={t('app.admin.manage_plan_category.description')} limit={100} />
|
||||
<FormRichText control={control} id="description" label={t('app.admin.plan_category_form.description')} limit={100} />
|
||||
|
||||
<FormInput id='weight' register={register} type='number' label={t('app.admin.manage_plan_category.significance')} />
|
||||
<FormInput id='weight' register={register} type='number' label={t('app.admin.plan_category_form.significance')} />
|
||||
<FabAlert level="info" className="significance-info">
|
||||
{t('app.admin.manage_plan_category.info')}
|
||||
{t('app.admin.plan_category_form.info')}
|
||||
</FabAlert>
|
||||
<FabButton type='submit'>{t(`app.admin.manage_plan_category.${action}_category.cta`)}</FabButton>
|
||||
<FabButton type='submit'>{t(`app.admin.plan_category_form.${action}.cta`)}</FabButton>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export const PlanCategoryForm: React.FC<PlanCategoryFormProps> = ({ action, category, onSuccess, onError }) => {
|
||||
const PlanCategoryFormWrapper: React.FC<PlanCategoryFormProps> = (props) => {
|
||||
return (
|
||||
<Loader>
|
||||
<PlanCategoryFormComponent action={action} category={category} onSuccess={onSuccess} onError={onError} />
|
||||
<PlanCategoryForm {...props} />
|
||||
</Loader>
|
||||
);
|
||||
};
|
||||
|
||||
export { PlanCategoryFormWrapper as PlanCategoryForm };
|
||||
|
@ -22,7 +22,7 @@ interface PlanCardProps {
|
||||
/**
|
||||
* This component is a "card" (visually), publicly presenting the details of a plan and allowing a user to subscribe.
|
||||
*/
|
||||
const PlanCardComponent: React.FC<PlanCardProps> = ({ plan, userId, subscribedPlanId, operator, onSelectPlan, isSelected, onLoginRequested, canSelectPlan }) => {
|
||||
const PlanCard: React.FC<PlanCardProps> = ({ plan, userId, subscribedPlanId, operator, onSelectPlan, isSelected, onLoginRequested, canSelectPlan }) => {
|
||||
const { t } = useTranslation('public');
|
||||
/**
|
||||
* Return the formatted localized amount of the given plan (eg. 20.5 => "20,50 €")
|
||||
@ -105,7 +105,7 @@ const PlanCardComponent: React.FC<PlanCardProps> = ({ plan, userId, subscribedPl
|
||||
<div className="content">
|
||||
{canBeScheduled() && <div className="wrap-monthly">
|
||||
<div className="price">
|
||||
<div className="amount">{t('app.public.plans.AMOUNT_per_month', { AMOUNT: monthlyAmount() })}</div>
|
||||
<div className="amount">{t('app.public.plan_card.AMOUNT_per_month', { AMOUNT: monthlyAmount() })}</div>
|
||||
<span className="period">{duration()}</span>
|
||||
</div>
|
||||
</div>}
|
||||
@ -118,25 +118,25 @@ const PlanCardComponent: React.FC<PlanCardProps> = ({ plan, userId, subscribedPl
|
||||
</div>
|
||||
<div className="card-footer">
|
||||
{hasDescription() && <div className="plan-description" dangerouslySetInnerHTML={{ __html: plan.description }}/>}
|
||||
{hasAttachment() && <a className="info-link" href={ plan.plan_file_url } target="_blank" rel="noreferrer">{ t('app.public.plans.more_information') }</a>}
|
||||
{hasAttachment() && <a className="info-link" href={ plan.plan_file_url } target="_blank" rel="noreferrer">{ t('app.public.plan_card.more_information') }</a>}
|
||||
{mustLogin() && <div className="cta-button">
|
||||
<button className="subscribe-button" onClick={handleLoginRequest}>{t('app.public.plans.i_subscribe_online')}</button>
|
||||
<button className="subscribe-button" onClick={handleLoginRequest}>{t('app.public.plan_card.i_subscribe_online')}</button>
|
||||
</div>}
|
||||
{canSubscribeForMe() && <div className="cta-button">
|
||||
{!hasSubscribedToThisPlan() && <button className={`subscribe-button ${isSelected ? 'selected-card' : ''}`}
|
||||
onClick={handleSelectPlan}
|
||||
disabled={!_.isNil(subscribedPlanId)}>
|
||||
{t('app.public.plans.i_choose_that_plan')}
|
||||
{t('app.public.plan_card.i_choose_that_plan')}
|
||||
</button>}
|
||||
{hasSubscribedToThisPlan() && <button className="subscribe-button selected-card" disabled>
|
||||
{ t('app.public.plans.i_already_subscribed') }
|
||||
{ t('app.public.plan_card.i_already_subscribed') }
|
||||
</button>}
|
||||
</div>}
|
||||
{canSubscribeForOther() && <div className="cta-button">
|
||||
<button className={`subscribe-button ${isSelected ? 'selected-card' : ''}`}
|
||||
onClick={handleSelectPlan}
|
||||
disabled={_.isNil(userId)}>
|
||||
<span>{ t('app.public.plans.i_choose_that_plan') }</span>
|
||||
<span>{ t('app.public.plan_card.i_choose_that_plan') }</span>
|
||||
</button>
|
||||
</div>}
|
||||
</div>
|
||||
@ -144,10 +144,12 @@ const PlanCardComponent: React.FC<PlanCardProps> = ({ plan, userId, subscribedPl
|
||||
);
|
||||
};
|
||||
|
||||
export const PlanCard: React.FC<PlanCardProps> = ({ plan, userId, subscribedPlanId, operator, onSelectPlan, isSelected, onLoginRequested, canSelectPlan }) => {
|
||||
const PlanCardWrapper: React.FC<PlanCardProps> = ({ plan, userId, subscribedPlanId, operator, onSelectPlan, isSelected, onLoginRequested, canSelectPlan }) => {
|
||||
return (
|
||||
<Loader>
|
||||
<PlanCardComponent plan={plan} userId={userId} subscribedPlanId={subscribedPlanId} operator={operator} isSelected={isSelected} onSelectPlan={onSelectPlan} onLoginRequested={onLoginRequested} canSelectPlan={canSelectPlan}/>
|
||||
<PlanCard plan={plan} userId={userId} subscribedPlanId={subscribedPlanId} operator={operator} isSelected={isSelected} onSelectPlan={onSelectPlan} onLoginRequested={onLoginRequested} canSelectPlan={canSelectPlan}/>
|
||||
</Loader>
|
||||
);
|
||||
};
|
||||
|
||||
export { PlanCardWrapper as PlanCard };
|
||||
|
@ -20,6 +20,9 @@ interface PlansFilterProps {
|
||||
*/
|
||||
type selectOption = { value: number, label: string };
|
||||
|
||||
/**
|
||||
* Allows filtering on plans list
|
||||
*/
|
||||
export const PlansFilter: React.FC<PlansFilterProps> = ({ user, groups, onGroupSelected, onError, onDurationSelected }) => {
|
||||
const { t } = useTranslation('public');
|
||||
|
||||
|
@ -31,7 +31,7 @@ type PlansTree = Map<number, Map<number, Array<Plan>>>;
|
||||
/**
|
||||
* This component display an organized list of plans to allow the end-user to select one and subscribe online
|
||||
*/
|
||||
const PlansList: React.FC<PlansListProps> = ({ onError, onPlanSelection, onLoginRequest, operator, customer, subscribedPlanId, canSelectPlan }) => {
|
||||
export const PlansList: React.FC<PlansListProps> = ({ onError, onPlanSelection, onLoginRequest, operator, customer, subscribedPlanId, canSelectPlan }) => {
|
||||
// all plans
|
||||
const [plans, setPlans] = useState<PlansTree>(null);
|
||||
// all plan-categories, ordered by weight
|
||||
|
@ -14,6 +14,7 @@ import { react2angular } from 'react2angular';
|
||||
import { IApplication } from '../../models/application';
|
||||
import { PrepaidPack } from '../../models/prepaid-pack';
|
||||
import PrepaidPackAPI from '../../api/prepaid-pack';
|
||||
import { FabAlert } from '../base/fab-alert';
|
||||
|
||||
declare const Application: IApplication;
|
||||
|
||||
@ -29,7 +30,11 @@ interface PacksSummaryProps {
|
||||
refresh?: Promise<void>
|
||||
}
|
||||
|
||||
const PacksSummaryComponent: React.FC<PacksSummaryProps> = ({ item, itemType, customer, operator, onError, onSuccess, refresh }) => {
|
||||
/**
|
||||
* Display a short summary of the prepaid-packs already bought by the provider customer, for the given item.
|
||||
* May also allows members to buy directly some new prepaid-packs.
|
||||
*/
|
||||
const PacksSummary: React.FC<PacksSummaryProps> = ({ item, itemType, customer, operator, onError, onSuccess, refresh }) => {
|
||||
const { t } = useTranslation('logged');
|
||||
|
||||
const [packs, setPacks] = useState<Array<PrepaidPack>>(null);
|
||||
@ -140,9 +145,9 @@ const PacksSummaryComponent: React.FC<PacksSummaryProps> = ({ item, itemType, cu
|
||||
<span className="remaining-hours">
|
||||
{t('app.logged.packs_summary.remaining_HOURS', { HOURS: totalHours(), ITEM: itemType })}
|
||||
{isPackOnlyForSubscription && !customer.subscribed_plan &&
|
||||
<div className="alert alert-warning m-t m-b">
|
||||
<FabAlert level="warning">
|
||||
{t('app.logged.packs_summary.unable_to_use_pack_for_subsription_is_expired')}
|
||||
</div>
|
||||
</FabAlert>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
@ -178,12 +183,14 @@ const PacksSummaryComponent: React.FC<PacksSummaryProps> = ({ item, itemType, cu
|
||||
);
|
||||
};
|
||||
|
||||
export const PacksSummary: React.FC<PacksSummaryProps> = ({ item, itemType, customer, operator, onError, onSuccess, refresh }) => {
|
||||
const PacksSummaryWrapper: React.FC<PacksSummaryProps> = (props) => {
|
||||
return (
|
||||
<Loader>
|
||||
<PacksSummaryComponent item={item} itemType={itemType} customer={customer} operator={operator} onError={onError} onSuccess={onSuccess} refresh={refresh} />
|
||||
<PacksSummary {...props} />
|
||||
</Loader>
|
||||
);
|
||||
};
|
||||
|
||||
Application.Components.component('packsSummary', react2angular(PacksSummary, ['item', 'itemType', 'customer', 'operator', 'onError', 'onSuccess', 'refresh']));
|
||||
export { PacksSummaryWrapper as PacksSummary };
|
||||
|
||||
Application.Components.component('packsSummary', react2angular(PacksSummaryWrapper, ['item', 'itemType', 'customer', 'operator', 'onError', 'onSuccess', 'refresh']));
|
||||
|
@ -30,6 +30,13 @@ export const EditablePrice: React.FC<EditablePriceProps> = ({ price, onSave }) =
|
||||
toggleEdit();
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when the user input a new price
|
||||
*/
|
||||
const handleChangePrice = (value: string): void => {
|
||||
setTempPrice(value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Enable or disable the edit mode
|
||||
*/
|
||||
@ -41,7 +48,7 @@ export const EditablePrice: React.FC<EditablePriceProps> = ({ price, onSave }) =
|
||||
<span className="editable-price">
|
||||
{!edit && <span className="display-price" onClick={toggleEdit}>{FormatLib.price(price.amount)}</span>}
|
||||
{edit && <span>
|
||||
<FabInput id="price" type="number" step={0.01} defaultValue={price.amount} addOn={Fablab.intl_currency} onChange={setTempPrice} required/>
|
||||
<FabInput id="price" type="number" step={0.01} defaultValue={price.amount} addOn={Fablab.intl_currency} onChange={handleChangePrice} required/>
|
||||
<FabButton icon={<i className="fas fa-check" />} className="approve-button" onClick={handleValidateEdit} />
|
||||
<FabButton icon={<i className="fas fa-times" />} className="cancel-button" onClick={toggleEdit} />
|
||||
</span>}
|
||||
|
@ -16,7 +16,7 @@ interface DeletePackProps {
|
||||
* This component shows a button.
|
||||
* When clicked, we show a modal dialog to ask the user for confirmation about the deletion of the provided pack.
|
||||
*/
|
||||
const DeletePackComponent: React.FC<DeletePackProps> = ({ onSuccess, onError, pack }) => {
|
||||
const DeletePack: React.FC<DeletePackProps> = ({ onSuccess, onError, pack }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
const [deletionModal, setDeletionModal] = useState<boolean>(false);
|
||||
@ -56,10 +56,12 @@ const DeletePackComponent: React.FC<DeletePackProps> = ({ onSuccess, onError, pa
|
||||
);
|
||||
};
|
||||
|
||||
export const DeletePack: React.FC<DeletePackProps> = ({ onSuccess, onError, pack }) => {
|
||||
const DeletePackWrapper: React.FC<DeletePackProps> = (props) => {
|
||||
return (
|
||||
<Loader>
|
||||
<DeletePackComponent onSuccess={onSuccess} onError={onError} pack={pack} />
|
||||
<DeletePack {...props} />
|
||||
</Loader>
|
||||
);
|
||||
};
|
||||
|
||||
export { DeletePackWrapper as DeletePack };
|
||||
|
@ -28,7 +28,7 @@ interface MachinesPricingProps {
|
||||
/**
|
||||
* Interface to set and edit the prices of machines-hours, per group
|
||||
*/
|
||||
const MachinesPricing: React.FC<MachinesPricingProps> = ({ onError, onSuccess }) => {
|
||||
export const MachinesPricing: React.FC<MachinesPricingProps> = ({ onError, onSuccess }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
const [machines, setMachines] = useState<Array<Machine>>(null);
|
||||
|
@ -8,7 +8,7 @@ import FormatLib from '../../../lib/format';
|
||||
import { EditExtendedPrice } from './edit-extended-price';
|
||||
import { DeleteExtendedPrice } from './delete-extended-price';
|
||||
|
||||
interface ConfigureExtendedPriceButtonProps {
|
||||
interface ConfigureExtendedPricesButtonProps {
|
||||
prices: Array<Price>,
|
||||
onError: (message: string) => void,
|
||||
onSuccess: (message: string) => void,
|
||||
@ -21,7 +21,7 @@ interface ConfigureExtendedPriceButtonProps {
|
||||
* This component is a button that shows the list of extendedPrices.
|
||||
* It also triggers modal dialogs to configure (add/edit/remove) extendedPrices.
|
||||
*/
|
||||
export const ConfigureExtendedPriceButton: React.FC<ConfigureExtendedPriceButtonProps> = ({ prices, onError, onSuccess, groupId, priceableId, priceableType }) => {
|
||||
export const ConfigureExtendedPricesButton: React.FC<ConfigureExtendedPricesButtonProps> = ({ prices, onError, onSuccess, groupId, priceableId, priceableType }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
const [extendedPrices, setExtendedPrices] = useState<Array<Price>>(prices);
|
@ -14,7 +14,7 @@ interface ExtendedPriceFormProps {
|
||||
}
|
||||
|
||||
/**
|
||||
* A form component to create/edit a extended price.
|
||||
* A form component to create/edit an extended price.
|
||||
* The form validation must be created elsewhere, using the attribute form={formId}.
|
||||
*/
|
||||
export const ExtendedPriceForm: React.FC<ExtendedPriceFormProps> = ({ formId, onSubmit, price }) => {
|
||||
|
@ -10,7 +10,7 @@ import { Group } from '../../../models/group';
|
||||
import { IApplication } from '../../../models/application';
|
||||
import { Space } from '../../../models/space';
|
||||
import { EditablePrice } from '../editable-price';
|
||||
import { ConfigureExtendedPriceButton } from './configure-extended-price-button';
|
||||
import { ConfigureExtendedPricesButton } from './configure-extended-prices-button';
|
||||
import PriceAPI from '../../../api/price';
|
||||
import { Price } from '../../../models/price';
|
||||
import { useImmer } from 'use-immer';
|
||||
@ -26,7 +26,7 @@ interface SpacesPricingProps {
|
||||
/**
|
||||
* Interface to set and edit the prices of spaces-hours, per group
|
||||
*/
|
||||
const SpacesPricing: React.FC<SpacesPricingProps> = ({ onError, onSuccess }) => {
|
||||
export const SpacesPricing: React.FC<SpacesPricingProps> = ({ onError, onSuccess }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
const [spaces, setSpaces] = useState<Array<Space>>(null);
|
||||
@ -120,7 +120,7 @@ const SpacesPricing: React.FC<SpacesPricingProps> = ({ onError, onSuccess }) =>
|
||||
<td>{space.name}</td>
|
||||
{groups?.map(group => <td key={group.id}>
|
||||
{prices.length && <EditablePrice price={findPriceBy(space.id, group.id)} onSave={handleUpdatePrice} />}
|
||||
<ConfigureExtendedPriceButton
|
||||
<ConfigureExtendedPricesButton
|
||||
prices={findExtendedPricesBy(space.id, group.id)}
|
||||
onError={onError}
|
||||
onSuccess={onSuccess}
|
||||
|
@ -20,6 +20,13 @@ interface ProfileFormOptionProps {
|
||||
onSuccess: (user: User) => void,
|
||||
}
|
||||
|
||||
/**
|
||||
* After first logged-in from an SSO, the user has two options:
|
||||
* - complete his profile (*) ;
|
||||
* - bind his profile to his existing account ;
|
||||
* (*) This component handle the first case.
|
||||
* It also deals with duplicate email addresses in database
|
||||
*/
|
||||
export const ProfileFormOption: React.FC<ProfileFormOptionProps> = ({ user, activeProvider, onError, onSuccess }) => {
|
||||
const { t } = useTranslation('logged');
|
||||
|
||||
|
@ -7,6 +7,7 @@ import { Loader } from '../base/loader';
|
||||
import { IApplication } from '../../models/application';
|
||||
import { ProfileCustomField } from '../../models/profile-custom-field';
|
||||
import ProfileCustomFieldAPI from '../../api/profile-custom-field';
|
||||
import { FabButton } from '../base/fab-button';
|
||||
|
||||
declare const Application: IApplication;
|
||||
|
||||
@ -18,7 +19,7 @@ interface ProfileCustomFieldsListProps {
|
||||
/**
|
||||
* This component shows a list of all profile custom fields
|
||||
*/
|
||||
const ProfileCustomFieldsList: React.FC<ProfileCustomFieldsListProps> = ({ onSuccess, onError }) => {
|
||||
export const ProfileCustomFieldsList: React.FC<ProfileCustomFieldsListProps> = ({ onSuccess, onError }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
const [profileCustomFields, setProfileCustomFields] = useState<Array<ProfileCustomField>>([]);
|
||||
@ -31,6 +32,9 @@ const ProfileCustomFieldsList: React.FC<ProfileCustomFieldsListProps> = ({ onSuc
|
||||
});
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Save the new state of the given custom field to the API
|
||||
*/
|
||||
const saveProfileCustomField = (profileCustomField: ProfileCustomField) => {
|
||||
ProfileCustomFieldAPI.update(profileCustomField).then(data => {
|
||||
const newFields = profileCustomFields.map(f => {
|
||||
@ -43,9 +47,9 @@ const ProfileCustomFieldsList: React.FC<ProfileCustomFieldsListProps> = ({ onSuc
|
||||
if (profileCustomFieldToEdit) {
|
||||
setProfileCustomFieldToEdit(null);
|
||||
}
|
||||
onSuccess(t('app.admin.settings.compte.organization_profile_custom_field_successfully_updated'));
|
||||
onSuccess(t('app.admin.settings.account.profile_custom_fields_list.field_successfully_updated'));
|
||||
}).catch(err => {
|
||||
onError(t('app.admin.settings.compte.organization_profile_custom_field_unable_to_update') + err);
|
||||
onError(t('app.admin.settings.account.profile_custom_fields_list.unable_to_update') + err);
|
||||
});
|
||||
};
|
||||
|
||||
@ -63,12 +67,19 @@ const ProfileCustomFieldsList: React.FC<ProfileCustomFieldsListProps> = ({ onSuc
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when the user clicks on the 'edit field' button.
|
||||
* Opens the edition form for the given custom field
|
||||
*/
|
||||
const editProfileCustomFieldLabel = (profileCustomField: ProfileCustomField) => {
|
||||
return () => {
|
||||
setProfileCustomFieldToEdit(_.clone(profileCustomField));
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when the input "label" is changed: updates the according state
|
||||
*/
|
||||
const onChangeProfileCustomFieldLabel = (e: BaseSyntheticEvent) => {
|
||||
const { value } = e.target;
|
||||
setProfileCustomFieldToEdit({
|
||||
@ -77,16 +88,22 @@ const ProfileCustomFieldsList: React.FC<ProfileCustomFieldsListProps> = ({ onSuc
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Save the currently edited custom field
|
||||
*/
|
||||
const saveProfileCustomFieldLabel = () => {
|
||||
saveProfileCustomField(profileCustomFieldToEdit);
|
||||
};
|
||||
|
||||
/**
|
||||
* Closes the edition form for the currently edited custom field
|
||||
*/
|
||||
const cancelEditProfileCustomFieldLabel = () => {
|
||||
setProfileCustomFieldToEdit(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<table className="table profile-custom-fields-list">
|
||||
<table className="profile-custom-fields-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: '50%' }}></th>
|
||||
@ -101,31 +118,44 @@ const ProfileCustomFieldsList: React.FC<ProfileCustomFieldsListProps> = ({ onSuc
|
||||
<td>
|
||||
{profileCustomFieldToEdit?.id !== field.id && field.label}
|
||||
{profileCustomFieldToEdit?.id !== field.id && (
|
||||
<button className="btn btn-default edit-profile-custom-field-label m-r-xs pull-right" onClick={editProfileCustomFieldLabel(field)}>
|
||||
<FabButton className="edit-field-button" onClick={editProfileCustomFieldLabel(field)}>
|
||||
<i className="fa fa-edit"></i>
|
||||
</button>
|
||||
</FabButton>
|
||||
)}
|
||||
{profileCustomFieldToEdit?.id === field.id && (
|
||||
<div>
|
||||
<input className="profile-custom-field-label-input" style={{ width: '80%', height: '38px' }} type="text" value={profileCustomFieldToEdit.label} onChange={onChangeProfileCustomFieldLabel} />
|
||||
<span className="buttons pull-right">
|
||||
<button className="btn btn-success save-profile-custom-field-label m-r-xs" onClick={saveProfileCustomFieldLabel}>
|
||||
<input className="edit-field-label-input"
|
||||
type="text" value={profileCustomFieldToEdit.label}
|
||||
onChange={onChangeProfileCustomFieldLabel} />
|
||||
<span className="buttons">
|
||||
<FabButton className="save-field-label" onClick={saveProfileCustomFieldLabel}>
|
||||
<i className="fa fa-check"></i>
|
||||
</button>
|
||||
<button className="btn btn-default delete-profile-custom-field-label m-r-xs" onClick={cancelEditProfileCustomFieldLabel}>
|
||||
</FabButton>
|
||||
<FabButton className="cancel-field-edition" onClick={cancelEditProfileCustomFieldLabel}>
|
||||
<i className="fa fa-ban"></i>
|
||||
</button>
|
||||
</FabButton>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<label htmlFor="profile-custom-field-actived" className="control-label m-r">{t('app.admin.settings.compte.organization_profile_custom_field.actived')}</label>
|
||||
<Switch checked={field.actived} id="profile-custom-field-actived" onChange={handleSwitchChanged(field, 'actived')} className="v-middle"></Switch>
|
||||
<td className="activated">
|
||||
<label htmlFor="profile-custom-field-actived">
|
||||
{t('app.admin.settings.account.profile_custom_fields_list.actived')}
|
||||
</label>
|
||||
<Switch checked={field.actived}
|
||||
id="profile-custom-field-actived"
|
||||
onChange={handleSwitchChanged(field, 'actived')}
|
||||
className="switch"></Switch>
|
||||
</td>
|
||||
<td>
|
||||
<label htmlFor="profile-custom-field-required" className="control-label m-r">{t('app.admin.settings.compte.organization_profile_custom_field.required')}</label>
|
||||
<Switch checked={field.required} disabled={!field.actived} id="profile-custom-field-required" onChange={handleSwitchChanged(field, 'required')} className="v-middle"></Switch>
|
||||
<td className="required">
|
||||
<label htmlFor="profile-custom-field-required">
|
||||
{t('app.admin.settings.account.profile_custom_fields_list.required')}
|
||||
</label>
|
||||
<Switch checked={field.required}
|
||||
disabled={!field.actived}
|
||||
id="profile-custom-field-required"
|
||||
onChange={handleSwitchChanged(field, 'required')}
|
||||
className="switch"></Switch>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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']));
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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']));
|
@ -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']));
|
@ -48,17 +48,21 @@ export const BooleanSetting: React.FC<BooleanSettingProps> = ({ name, label, cla
|
||||
*/
|
||||
const updateSetting = () => {
|
||||
SettingAPI.update(name, value ? 'true' : 'false')
|
||||
.then(() => onSuccess(t('app.admin.settings.customization_of_SETTING_successfully_saved', { SETTING: t(`app.admin.settings.${name}`) })))
|
||||
.then(() => onSuccess(t('app.admin.boolean_setting.customization_of_SETTING_successfully_saved', {
|
||||
SETTING: t(`app.admin.settings.${name}`) // eslint-disable-line fabmanager/scoped-translation
|
||||
})))
|
||||
.catch(err => {
|
||||
if (err.status === 304) return;
|
||||
|
||||
if (err.status === 423) {
|
||||
onError(t('app.admin.settings.error_SETTING_locked', { SETTING: t(`app.admin.settings.${name}`) }));
|
||||
onError(t('app.admin.boolean_setting.error_SETTING_locked', {
|
||||
SETTING: t(`app.admin.settings.${name}`) // eslint-disable-line fabmanager/scoped-translation
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(err);
|
||||
onError(t('app.admin.settings.an_error_occurred_saving_the_setting'));
|
||||
onError(t('app.admin.boolean_setting.an_error_occurred_saving_the_setting'));
|
||||
});
|
||||
};
|
||||
|
||||
@ -97,15 +101,15 @@ export const BooleanSetting: React.FC<BooleanSettingProps> = ({ name, label, cla
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`form-group ${className || ''}`}>
|
||||
<label htmlFor={`setting-${name}`} className="control-label m-r">{label}</label>
|
||||
<Switch checked={value} id={`setting-${name}}`} onChange={handleChanged} className="v-middle"></Switch>
|
||||
{!hideSave && <FabButton className="btn btn-warning m-l" onClick={handleSave}>{t('app.admin.check_list_setting.save')}</FabButton> }
|
||||
<div className={`boolean-setting ${className || ''}`}>
|
||||
<label htmlFor={`setting-${name}`}>{label}</label>
|
||||
<Switch checked={value} id={`setting-${name}}`} onChange={handleChanged} className="switch"></Switch>
|
||||
{!hideSave && <FabButton className="save-btn" onClick={handleSave}>{t('app.admin.boolean_setting.save')}</FabButton> }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const BooleanSettingWrapper: React.FC<BooleanSettingProps> = ({ onChange, onSuccess, onError, label, className, name, hideSave, onBeforeSave }) => {
|
||||
const BooleanSettingWrapper: React.FC<BooleanSettingProps> = ({ onChange, onSuccess, onError, label, className, name, hideSave, onBeforeSave }) => {
|
||||
return (
|
||||
<Loader>
|
||||
<BooleanSetting label={label} name={name} onError={onError} onSuccess={onSuccess} onChange={onChange} className={className} hideSave={hideSave} onBeforeSave={onBeforeSave} />
|
||||
|
@ -75,7 +75,9 @@ export const CheckListSetting: React.FC<CheckListSettingProps> = ({ name, label,
|
||||
*/
|
||||
const handleSave = () => {
|
||||
SettingAPI.update(name, value)
|
||||
.then(() => onSuccess(t('app.admin.check_list_setting.customization_of_SETTING_successfully_saved', { SETTING: t(`app.admin.settings.${name}`) })))
|
||||
.then(() => onSuccess(t('app.admin.check_list_setting.customization_of_SETTING_successfully_saved', {
|
||||
SETTING: t(`app.admin.settings.${name}`) // eslint-disable-line fabmanager/scoped-translation
|
||||
})))
|
||||
.catch(err => onError(err));
|
||||
};
|
||||
|
||||
@ -98,7 +100,7 @@ export const CheckListSetting: React.FC<CheckListSettingProps> = ({ name, label,
|
||||
);
|
||||
};
|
||||
|
||||
export const CheckListSettingWrapper: React.FC<CheckListSettingProps> = ({ availableOptions, onSuccess, onError, label, className, name, hideSave, defaultValue, onChange }) => {
|
||||
const CheckListSettingWrapper: React.FC<CheckListSettingProps> = ({ availableOptions, onSuccess, onError, label, className, name, hideSave, defaultValue, onChange }) => {
|
||||
return (
|
||||
<Loader>
|
||||
<CheckListSetting availableOptions={availableOptions} label={label} name={name} onError={onError} onSuccess={onSuccess} className={className} hideSave={hideSave} defaultValue={defaultValue} onChange={onChange} />
|
||||
|
@ -8,6 +8,7 @@ import { Loader } from '../base/loader';
|
||||
import { FabButton } from '../base/fab-button';
|
||||
import { BooleanSetting } from './boolean-setting';
|
||||
import { CheckListSetting } from './check-list-setting';
|
||||
import { FabAlert } from '../base/fab-alert';
|
||||
|
||||
declare const Application: IApplication;
|
||||
|
||||
@ -17,16 +18,16 @@ interface UserValidationSettingProps {
|
||||
}
|
||||
|
||||
/**
|
||||
* This component allows to configure user validation required setting.
|
||||
* This component allows an admin to configure the settings related to the user account validation.
|
||||
*/
|
||||
const UserValidationSetting: React.FC<UserValidationSettingProps> = ({ onSuccess, onError }) => {
|
||||
export const UserValidationSetting: React.FC<UserValidationSettingProps> = ({ onSuccess, onError }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
const [userValidationRequired, setUserValidationRequired] = useState<string>('false');
|
||||
const userValidationRequiredListDefault = ['subscription', 'machine', 'event', 'space', 'training', 'pack'];
|
||||
const [userValidationRequiredList, setUserValidationRequiredList] = useState<string>(null);
|
||||
const userValidationRequiredOptions = userValidationRequiredListDefault.map(l => {
|
||||
return [l, t(`app.admin.settings.compte.user_validation_required_list.${l}`)];
|
||||
return [l, t(`app.admin.settings.account.user_validation_setting.user_validation_required_list.${l}`)];
|
||||
});
|
||||
|
||||
/**
|
||||
@ -36,20 +37,24 @@ const UserValidationSetting: React.FC<UserValidationSettingProps> = ({ onSuccess
|
||||
SettingAPI.update(name, value)
|
||||
.then(() => {
|
||||
if (name === SettingName.UserValidationRequired) {
|
||||
onSuccess(t('app.admin.settings.customization_of_SETTING_successfully_saved', { SETTING: t(`app.admin.settings.compte.${name}`) }));
|
||||
onSuccess(t('app.admin.settings.account.user_validation_setting.customization_of_SETTING_successfully_saved', {
|
||||
SETTING: t(`app.admin.settings.account.${name}`) // eslint-disable-line fabmanager/scoped-translation
|
||||
}));
|
||||
}
|
||||
}).catch(err => {
|
||||
if (err.status === 304) return;
|
||||
|
||||
if (err.status === 423) {
|
||||
if (name === SettingName.UserValidationRequired) {
|
||||
onError(t('app.admin.settings.error_SETTING_locked', { SETTING: t(`app.admin.settings.compte.${name}`) }));
|
||||
onError(t('app.admin.settings.account.user_validation_setting.error_SETTING_locked', {
|
||||
SETTING: t(`app.admin.settings.account.${name}`) // eslint-disable-line fabmanager/scoped-translation
|
||||
}));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(err);
|
||||
onError(t('app.admin.settings.an_error_occurred_saving_the_setting'));
|
||||
onError(t('app.admin.settings.account.user_validation_setting.an_error_occurred_saving_the_setting'));
|
||||
});
|
||||
};
|
||||
|
||||
@ -70,7 +75,7 @@ const UserValidationSetting: React.FC<UserValidationSettingProps> = ({ onSuccess
|
||||
return (
|
||||
<div className="user-validation-setting">
|
||||
<BooleanSetting name={SettingName.UserValidationRequired}
|
||||
label={t('app.admin.settings.compte.user_validation_required_option_label')}
|
||||
label={t('app.admin.settings.account.user_validation_setting.user_validation_required_option_label')}
|
||||
hideSave={true}
|
||||
onChange={setUserValidationRequired}
|
||||
onSuccess={onSuccess}
|
||||
@ -78,13 +83,13 @@ const UserValidationSetting: React.FC<UserValidationSettingProps> = ({ onSuccess
|
||||
</BooleanSetting>
|
||||
{userValidationRequired === 'true' &&
|
||||
<div>
|
||||
<h4>{t('app.admin.settings.compte.user_validation_required_list_title')}</h4>
|
||||
<h4>{t('app.admin.settings.account.user_validation_setting.user_validation_required_list_title')}</h4>
|
||||
<p>
|
||||
{t('app.admin.settings.compte.user_validation_required_list_info')}
|
||||
</p>
|
||||
<p className="alert alert-warning">
|
||||
{t('app.admin.settings.compte.user_validation_required_list_other_info')}
|
||||
{t('app.admin.settings.account.user_validation_setting.user_validation_required_list_info')}
|
||||
</p>
|
||||
<FabAlert level="warning">
|
||||
{t('app.admin.settings.account.user_validation_setting.user_validation_required_list_other_info')}
|
||||
</FabAlert>
|
||||
<CheckListSetting name={SettingName.UserValidationRequiredList}
|
||||
label=""
|
||||
availableOptions={userValidationRequiredOptions}
|
||||
@ -96,7 +101,7 @@ const UserValidationSetting: React.FC<UserValidationSettingProps> = ({ onSuccess
|
||||
</CheckListSetting>
|
||||
</div>
|
||||
}
|
||||
<FabButton className="btn btn-warning m-t" onClick={handleSave}>{t('app.admin.check_list_setting.save')}</FabButton>
|
||||
<FabButton className="save-btn" onClick={handleSave}>{t('app.admin.settings.account.user_validation_setting.save')}</FabButton>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -16,6 +16,9 @@ interface EditSocialsProps<TFieldValues> {
|
||||
disabled: boolean|((id: string) => boolean),
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow a user to edit its personnal social networks
|
||||
*/
|
||||
export const EditSocials = <TFieldValues extends FieldValues>({ register, setValue, networks, formState, disabled }: EditSocialsProps<TFieldValues>) => {
|
||||
const { t } = useTranslation('shared');
|
||||
// regular expression to validate the the input fields
|
||||
@ -23,10 +26,17 @@ export const EditSocials = <TFieldValues extends FieldValues>({ register, setVal
|
||||
|
||||
const initSelectedNetworks = networks.filter(el => !['', null, undefined].includes(el.url));
|
||||
const [selectedNetworks, setSelectedNetworks] = useState(initSelectedNetworks);
|
||||
|
||||
/**
|
||||
* Callback triggered when the user adds a network, from the list of available networks, to the editable networks.
|
||||
*/
|
||||
const selectNetwork = (network) => {
|
||||
setSelectedNetworks([...selectedNetworks, network]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return a derivated state of the selected networks list, depending on the given action.
|
||||
*/
|
||||
const reducer = (state, action) => {
|
||||
switch (action.type) {
|
||||
case 'delete':
|
||||
@ -61,14 +71,14 @@ export const EditSocials = <TFieldValues extends FieldValues>({ register, setVal
|
||||
rules= {{
|
||||
pattern: {
|
||||
value: urlRegex,
|
||||
message: t('app.shared.user_profile_form.website_invalid')
|
||||
message: t('app.shared.edit_socials.website_invalid')
|
||||
}
|
||||
}}
|
||||
formState={formState}
|
||||
defaultValue={network.url}
|
||||
label={network.name}
|
||||
disabled={disabled}
|
||||
placeholder={t('app.shared.text_editor.url_placeholder')}
|
||||
placeholder={t('app.shared.edit_socials.url_placeholder')}
|
||||
icon={<img src={`${Icons}#${network.name}`}></img>}
|
||||
addOn={<Trash size={16} />}
|
||||
addOnAction={() => dispatch({ type: 'delete', payload: { network, field: `profile_attributes.${network.name}` } })} />
|
||||
|
@ -20,6 +20,9 @@ interface FabSocialsProps {
|
||||
onSuccess: (message: string) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows the Fablab to edit its corporate social networks, or to display them read-only to the end users (show=true)
|
||||
*/
|
||||
export const FabSocials: React.FC<FabSocialsProps> = ({ show = false, onError, onSuccess }) => {
|
||||
const { t } = useTranslation('shared');
|
||||
// regular expression to validate the the input fields
|
||||
@ -42,6 +45,9 @@ export const FabSocials: React.FC<FabSocialsProps> = ({ show = false, onError, o
|
||||
setSelectedNetworks(fabNetworks.filter(el => el.url !== ''));
|
||||
}, [fabNetworks]);
|
||||
|
||||
/**
|
||||
* Callback triggered when the social networks are saved
|
||||
*/
|
||||
const onSubmit = (data) => {
|
||||
const updatedNetworks = new Map<SettingName, string>();
|
||||
Object.keys(data).forEach(key => updatedNetworks.set(key as SettingName, data[key]));
|
||||
@ -55,17 +61,24 @@ export const FabSocials: React.FC<FabSocialsProps> = ({ show = false, onError, o
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when the user adds a network, from the list of available networks, to the editable networks.
|
||||
*/
|
||||
const selectNetwork = (network) => {
|
||||
setSelectedNetworks([...selectedNetworks, network]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when the user removes a network, from the list of editables networks, add put it back to the
|
||||
* list of avaiable networks.
|
||||
*/
|
||||
const remove = (network) => {
|
||||
setSelectedNetworks(selectedNetworks.filter(el => el !== network));
|
||||
setValue(network.name, '');
|
||||
};
|
||||
|
||||
return (
|
||||
<>{show
|
||||
<div className="fab-socials">{show
|
||||
? (selectedNetworks.length > 0) && <>
|
||||
<h2>{t('app.shared.fab_socials.follow_us')}</h2>
|
||||
<div className='social-icons'>
|
||||
@ -94,7 +107,7 @@ export const FabSocials: React.FC<FabSocialsProps> = ({ show = false, onError, o
|
||||
rules={{
|
||||
pattern: {
|
||||
value: urlRegex,
|
||||
message: t('app.shared.user_profile_form.website_invalid')
|
||||
message: t('app.shared.fab_socials.website_invalid')
|
||||
}
|
||||
}}
|
||||
formState={formState}
|
||||
@ -107,11 +120,11 @@ export const FabSocials: React.FC<FabSocialsProps> = ({ show = false, onError, o
|
||||
)}
|
||||
</div>}
|
||||
<FabButton type='submit'
|
||||
className='btn-warning'>
|
||||
{t('app.shared.buttons.save')}
|
||||
className='save-btn'>
|
||||
{t('app.shared.fab_socials.save')}
|
||||
</FabButton>
|
||||
</form>
|
||||
}</>
|
||||
}</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -26,7 +26,7 @@ interface FreeExtendModalProps {
|
||||
/**
|
||||
* Modal dialog shown to extend the current subscription of a customer, for free
|
||||
*/
|
||||
const FreeExtendModal: React.FC<FreeExtendModalProps> = ({ isOpen, toggleModal, subscription, customerId, onError, onSuccess }) => {
|
||||
export const FreeExtendModal: React.FC<FreeExtendModalProps> = ({ isOpen, toggleModal, subscription, customerId, onError, onSuccess }) => {
|
||||
// we do not render the modal if the subscription was not provided
|
||||
if (!subscription) return null;
|
||||
|
||||
|
@ -35,7 +35,7 @@ interface RenewModalProps {
|
||||
/**
|
||||
* Modal dialog shown to renew the current subscription of a customer, for free
|
||||
*/
|
||||
const RenewModal: React.FC<RenewModalProps> = ({ isOpen, toggleModal, subscription, customer, operator, onError, onSuccess }) => {
|
||||
export const RenewModal: React.FC<RenewModalProps> = ({ isOpen, toggleModal, subscription, customer, operator, onError, onSuccess }) => {
|
||||
// we do not render the modal if the subscription was not provided
|
||||
if (!subscription) return null;
|
||||
|
||||
@ -85,14 +85,14 @@ const RenewModal: React.FC<RenewModalProps> = ({ isOpen, toggleModal, subscripti
|
||||
* Return the formatted localized date for the given date
|
||||
*/
|
||||
const formatDateTime = (date: Date|TDateISO): string => {
|
||||
return t('app.admin.free_extend_modal.DATE_TIME', { DATE: FormatLib.date(date), TIME: FormatLib.time(date) });
|
||||
return t('app.admin.renew_modal.DATE_TIME', { DATE: FormatLib.date(date), TIME: FormatLib.time(date) });
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when the payment of the subscription renewal was successful
|
||||
*/
|
||||
const onPaymentSuccess = (): void => {
|
||||
onSuccess(t('app.admin.renew_subscription_modal.renew_success'), expirationDate);
|
||||
onSuccess(t('app.admin.renew_modal.renew_success'), expirationDate);
|
||||
toggleModal();
|
||||
};
|
||||
|
||||
@ -108,25 +108,25 @@ const RenewModal: React.FC<RenewModalProps> = ({ isOpen, toggleModal, subscripti
|
||||
toggleModal={toggleModal}
|
||||
width={ModalSize.large}
|
||||
className="renew-modal"
|
||||
title={t('app.admin.renew_subscription_modal.renew_subscription')}
|
||||
confirmButton={t('app.admin.renew_subscription_modal.renew')}
|
||||
title={t('app.admin.renew_modal.renew_subscription')}
|
||||
confirmButton={t('app.admin.renew_modal.renew')}
|
||||
onConfirm={toggleLocalPaymentModal}
|
||||
closeButton>
|
||||
<FabAlert level="danger" className="conditions">
|
||||
<p>{t('app.admin.renew_subscription_modal.renew_subscription_info')}</p>
|
||||
<p>{t('app.admin.renew_subscription_modal.credits_will_be_reset')}</p>
|
||||
<p>{t('app.admin.renew_modal.renew_subscription_info')}</p>
|
||||
<p>{t('app.admin.renew_modal.credits_will_be_reset')}</p>
|
||||
</FabAlert>
|
||||
<div className="form-and-payment">
|
||||
<form className="configuration-form">
|
||||
<label htmlFor="current_expiration">{t('app.admin.renew_subscription_modal.current_expiration')}</label>
|
||||
<label htmlFor="current_expiration">{t('app.admin.renew_modal.current_expiration')}</label>
|
||||
<FabInput id="current_expiration"
|
||||
defaultValue={formatDateTime(subscription.expired_at)}
|
||||
readOnly />
|
||||
<label htmlFor="new_start">{t('app.admin.renew_subscription_modal.new_start')}</label>
|
||||
<label htmlFor="new_start">{t('app.admin.renew_modal.new_start')}</label>
|
||||
<FabInput id="new_start"
|
||||
defaultValue={formatDateTime(subscription.expired_at)}
|
||||
readOnly />
|
||||
<label htmlFor="new_expiration">{t('app.admin.renew_subscription_modal.new_expiration_date')}</label>
|
||||
<label htmlFor="new_expiration">{t('app.admin.renew_modal.new_expiration_date')}</label>
|
||||
<FabInput id="new_expiration"
|
||||
defaultValue={formatDateTime(expirationDate)}
|
||||
readOnly/>
|
||||
@ -135,7 +135,7 @@ const RenewModal: React.FC<RenewModalProps> = ({ isOpen, toggleModal, subscripti
|
||||
{subscription.plan.monthly_payment && <SelectSchedule show selected={scheduleRequired} onChange={setScheduleRequired} />}
|
||||
{price?.schedule && <PaymentScheduleSummary schedule={price.schedule as PaymentSchedule} />}
|
||||
{price && !price?.schedule && <div className="one-go-payment">
|
||||
<h4>{t('app.admin.renew_subscription_modal.pay_in_one_go')}</h4>
|
||||
<h4>{t('app.admin.renew_modal.pay_in_one_go')}</h4>
|
||||
<span>{FormatLib.price(price.price)}</span>
|
||||
</div>}
|
||||
</div>
|
||||
|
@ -39,7 +39,7 @@ type selectOption = { value: number, label: string };
|
||||
/**
|
||||
* Modal dialog shown to create a subscription for the given customer
|
||||
*/
|
||||
const SubscribeModal: React.FC<SubscribeModalProps> = ({ isOpen, toggleModal, customer, operator, onError, onSuccess }) => {
|
||||
export const SubscribeModal: React.FC<SubscribeModalProps> = ({ isOpen, toggleModal, customer, operator, onError, onSuccess }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
const [selectedPlan, setSelectedPlan] = useState<Plan>(null);
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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']));
|
@ -2,22 +2,22 @@ import React, { BaseSyntheticEvent, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ProofOfIdentityType } from '../../models/proof-of-identity-type';
|
||||
|
||||
interface ProofOfIdentityRefusalFormProps {
|
||||
interface SupportingDocumentsRefusalFormProps {
|
||||
proofOfIdentityTypes: Array<ProofOfIdentityType>,
|
||||
onChange: (field: string, value: string | Array<number>) => void,
|
||||
}
|
||||
|
||||
/**
|
||||
* Form to set the stripe's public and private keys
|
||||
* Form to set the refuse the uploaded supporting documents
|
||||
*/
|
||||
export const ProofOfIdentityRefusalForm: React.FC<ProofOfIdentityRefusalFormProps> = ({ proofOfIdentityTypes, onChange }) => {
|
||||
export const SupportingDocumentsRefusalForm: React.FC<SupportingDocumentsRefusalFormProps> = ({ proofOfIdentityTypes, onChange }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
const [values, setValues] = useState<Array<number>>([]);
|
||||
const [message, setMessage] = useState<string>('');
|
||||
|
||||
/**
|
||||
* Callback triggered when the name has changed.
|
||||
* Callback triggered when the message has changed.
|
||||
*/
|
||||
const handleMessageChange = (e: BaseSyntheticEvent): void => {
|
||||
const { value } = e.target;
|
||||
@ -26,10 +26,9 @@ export const ProofOfIdentityRefusalForm: React.FC<ProofOfIdentityRefusalFormProp
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when a checkbox is ticked or unticked.
|
||||
* This function construct the resulting string, by adding or deleting the provided option identifier.
|
||||
* Callback triggered when the document type checkbox is ticked or unticked.
|
||||
*/
|
||||
const handleProofOfIdnentityTypesChange = (value: number) => {
|
||||
const handleTypeSelectionChange = (value: number) => {
|
||||
return (event: BaseSyntheticEvent) => {
|
||||
let newValues: Array<number>;
|
||||
if (event.target.checked) {
|
||||
@ -43,27 +42,32 @@ export const ProofOfIdentityRefusalForm: React.FC<ProofOfIdentityRefusalFormProp
|
||||
};
|
||||
|
||||
/**
|
||||
* Verify if the provided option is currently ticked (i.e. included in the value string)
|
||||
* Verify if the provided type is currently ticked (i.e. about to be refused)
|
||||
*/
|
||||
const isChecked = (value: number) => {
|
||||
return values.includes(value);
|
||||
const isChecked = (typeId: number) => {
|
||||
return values.includes(typeId);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="proof-of-identity-type-form">
|
||||
<div className="supporting-documents-refusal-form">
|
||||
<form name="proofOfIdentityRefusalForm">
|
||||
<div>
|
||||
{proofOfIdentityTypes.map(type => <div key={type.id} className="">
|
||||
{proofOfIdentityTypes.map(type => <div key={type.id}>
|
||||
<label htmlFor={`checkbox-${type.id}`}>{type.name}</label>
|
||||
<input id={`checkbox-${type.id}`} className="pull-right" type="checkbox" checked={isChecked(type.id)} onChange={handleProofOfIdnentityTypesChange(type.id)} />
|
||||
<input id={`checkbox-${type.id}`}
|
||||
type="checkbox"
|
||||
checked={isChecked(type.id)}
|
||||
onChange={handleTypeSelectionChange(type.id)} />
|
||||
</div>)}
|
||||
</div>
|
||||
<div className="proof-of-identity-refusal-comment-textarea m-t">
|
||||
<label htmlFor="proof-of-identity-refusal-comment">{t('app.admin.members_edit.proof_of_identity_refusal_comment')}</label>
|
||||
<div className="refusal-comment">
|
||||
<label htmlFor="proof-of-identity-refusal-comment">
|
||||
{t('app.admin.supporting_documents_refusal_form.refusal_comment')}
|
||||
</label>
|
||||
<textarea
|
||||
id="proof-of-identity-refusal-comment"
|
||||
value={message}
|
||||
placeholder={t('app.admin.members_edit.proof_of_identity_refuse_input_message')}
|
||||
placeholder={t('app.admin.supporting_documents_refusal_form.comment_placeholder')}
|
||||
onChange={handleMessageChange}
|
||||
style={{ width: '100%' }}
|
||||
rows={5}
|
@ -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>
|
||||
);
|
||||
};
|
@ -5,7 +5,7 @@ import { FabInput } from '../base/fab-input';
|
||||
import { ProofOfIdentityType } from '../../models/proof-of-identity-type';
|
||||
import { Group } from '../../models/group';
|
||||
|
||||
interface ProofOfIdentityTypeFormProps {
|
||||
interface SupportingDocumentsTypeFormProps {
|
||||
groups: Array<Group>,
|
||||
proofOfIdentityType?: ProofOfIdentityType,
|
||||
onChange: (field: string, value: string | Array<number>) => void,
|
||||
@ -18,13 +18,13 @@ interface ProofOfIdentityTypeFormProps {
|
||||
type selectOption = { value: number, label: string };
|
||||
|
||||
/**
|
||||
* Form to set the stripe's public and private keys
|
||||
* Form to set create/edit supporting documents type
|
||||
*/
|
||||
export const ProofOfIdentityTypeForm: React.FC<ProofOfIdentityTypeFormProps> = ({ groups, proofOfIdentityType, onChange }) => {
|
||||
export const SupportingDocumentsTypeForm: React.FC<SupportingDocumentsTypeFormProps> = ({ groups, proofOfIdentityType, onChange }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
/**
|
||||
* Convert all themes to the react-select format
|
||||
* Convert all groups to the react-select format
|
||||
*/
|
||||
const buildOptions = (): Array<selectOption> => {
|
||||
return groups.map(t => {
|
||||
@ -33,7 +33,7 @@ export const ProofOfIdentityTypeForm: React.FC<ProofOfIdentityTypeFormProps> = (
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the current groups(s), formatted to match the react-select format
|
||||
* Return the group(s) associated with the current type, formatted to match the react-select format
|
||||
*/
|
||||
const groupsValues = (): Array<selectOption> => {
|
||||
const res = [];
|
||||
@ -63,23 +63,23 @@ export const ProofOfIdentityTypeForm: React.FC<ProofOfIdentityTypeFormProps> = (
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="proof-of-identity-type-form">
|
||||
<div className="proof-of-identity-type-form-info">
|
||||
{t('app.admin.settings.compte.proof_of_identity_type_form_info')}
|
||||
<div className="supporting-documents-type-form">
|
||||
<div className="info-area">
|
||||
{t('app.admin.settings.account.supporting_documents_type_form.type_form_info')}
|
||||
</div>
|
||||
<form name="proofOfIdentityTypeForm">
|
||||
<div className="proof-of-identity-type-select m-t">
|
||||
<div className="field">
|
||||
<Select defaultValue={groupsValues()}
|
||||
placeholder={t('app.admin.settings.compte.proof_of_identity_type_select_group')}
|
||||
placeholder={t('app.admin.settings.account.supporting_documents_type_form.select_group')}
|
||||
onChange={handleGroupsChange}
|
||||
options={buildOptions()}
|
||||
isMulti />
|
||||
</div>
|
||||
<div className="proof-of-identity-type-input m-t">
|
||||
<div className="field">
|
||||
<FabInput id="proof_of_identity_type_name"
|
||||
icon={<i className="fa fa-edit" />}
|
||||
defaultValue={proofOfIdentityType?.name || ''}
|
||||
placeholder={t('app.admin.settings.compte.proof_of_identity_type_input_name')}
|
||||
placeholder={t('app.admin.settings.account.supporting_documents_type_form.name')}
|
||||
onChange={handleNameChange}
|
||||
debounce={200}
|
||||
required/>
|
@ -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>
|
||||
);
|
||||
};
|
@ -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']));
|
@ -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']));
|
@ -28,6 +28,8 @@ import TagAPI from '../../api/tag';
|
||||
import { FormMultiSelect } from '../form/form-multi-select';
|
||||
import ProfileCustomFieldAPI from '../../api/profile-custom-field';
|
||||
import { ProfileCustomField } from '../../models/profile-custom-field';
|
||||
import { SettingName } from '../../models/setting';
|
||||
import SettingAPI from '../../api/setting';
|
||||
|
||||
declare const Application: IApplication;
|
||||
|
||||
@ -68,6 +70,7 @@ export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size,
|
||||
const [groups, setGroups] = useState<selectOption[]>([]);
|
||||
const [termsAndConditions, setTermsAndConditions] = useState<CustomAsset>(null);
|
||||
const [profileCustomFields, setProfileCustomFields] = useState<ProfileCustomField[]>([]);
|
||||
const [requiredFieldsSettings, setRequiredFieldsSettings] = useState<Map<SettingName, string>>(new Map());
|
||||
|
||||
useEffect(() => {
|
||||
AuthProviderAPI.active().then(data => {
|
||||
@ -94,6 +97,9 @@ export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size,
|
||||
});
|
||||
setValue('invoicing_profile_attributes.user_profile_custom_fields_attributes', userProfileCustomFields);
|
||||
}).catch(error => onError(error));
|
||||
SettingAPI.query([SettingName.PhoneRequired, SettingName.AddressRequired])
|
||||
.then(settings => setRequiredFieldsSettings(settings))
|
||||
.catch(error => onError(error));
|
||||
}, []);
|
||||
|
||||
/**
|
||||
@ -202,6 +208,7 @@ export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size,
|
||||
register={register}
|
||||
label={t('app.shared.user_profile_form.date_of_birth')}
|
||||
disabled={isDisabled}
|
||||
rules={{ required: true }}
|
||||
type="date" />
|
||||
<FormInput id="profile_attributes.phone"
|
||||
register={register}
|
||||
@ -209,7 +216,8 @@ export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size,
|
||||
pattern: {
|
||||
value: phoneRegex,
|
||||
message: t('app.shared.user_profile_form.phone_number_invalid')
|
||||
}
|
||||
},
|
||||
required: requiredFieldsSettings.get(SettingName.PhoneRequired) === 'true'
|
||||
}}
|
||||
disabled={isDisabled}
|
||||
formState={formState}
|
||||
@ -222,6 +230,7 @@ export const UserProfileForm: React.FC<UserProfileFormProps> = ({ action, size,
|
||||
<FormInput id="invoicing_profile_attributes.address_attributes.address"
|
||||
register={register}
|
||||
disabled={isDisabled}
|
||||
rules={{ required: requiredFieldsSettings.get(SettingName.AddressRequired) === 'true' }}
|
||||
label={t('app.shared.user_profile_form.address')} />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -6,6 +6,7 @@ import { User } from '../../models/user';
|
||||
import { IApplication } from '../../models/application';
|
||||
import { react2angular } from 'react2angular';
|
||||
import MemberAPI from '../../api/member';
|
||||
import { TDateISO } from '../../typings/date-iso';
|
||||
|
||||
declare const Application: IApplication;
|
||||
|
||||
@ -34,23 +35,23 @@ export const UserValidation: React.FC<UserValidationProps> = ({ member, onSucces
|
||||
setValue(_value);
|
||||
const _member = _.clone(member);
|
||||
if (_value) {
|
||||
_member.validated_at = new Date();
|
||||
_member.validated_at = new Date().toISOString() as TDateISO;
|
||||
} else {
|
||||
_member.validated_at = null;
|
||||
}
|
||||
MemberAPI.validate(_member)
|
||||
.then((user: User) => {
|
||||
onSuccess(user, t(`app.admin.members_edit.${_value ? 'validate' : 'invalidate'}_member_success`));
|
||||
onSuccess(user, t(`app.admin.user_validation.${_value ? 'validate' : 'invalidate'}_member_success`));
|
||||
}).catch(err => {
|
||||
setValue(!_value);
|
||||
onError(t(`app.admin.members_edit.${_value ? 'validate' : 'invalidate'}_member_error`) + err);
|
||||
onError(t(`app.admin.user_validation.${_value ? 'validate' : 'invalidate'}_member_error`) + err);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="user-validation">
|
||||
<label htmlFor="user-validation-switch" className="control-label m-r">{t('app.admin.members_edit.validate_account')}</label>
|
||||
<Switch checked={value} id="user-validation-switch" onChange={handleChanged} className="v-middle"></Switch>
|
||||
<label htmlFor="user-validation-switch">{t('app.admin.user_validation.validate_account')}</label>
|
||||
<Switch checked={value} id="user-validation-switch" onChange={handleChanged} className="switch"></Switch>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -929,9 +929,6 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state',
|
||||
const initialize = function () {
|
||||
CSRF.setMetaTags();
|
||||
|
||||
// init the birthdate to JS object
|
||||
$scope.user.statistic_profile_attributes.birthday = moment($scope.user.statistic_profile_attributes.birthday).toDate();
|
||||
|
||||
// the user subscription
|
||||
if (($scope.user.subscribed_plan != null) && ($scope.user.subscription != null)) {
|
||||
$scope.subscription = $scope.user.subscription;
|
||||
|
@ -72,6 +72,7 @@ Application.Controllers.controller('EventsController', ['$scope', '$state', 'Eve
|
||||
// reinitialize results datasets
|
||||
$scope.page = 1;
|
||||
$scope.eventsGroupByMonth = {};
|
||||
$scope.featuredEevent = null;
|
||||
$scope.events = [];
|
||||
$scope.monthOrder = [];
|
||||
$scope.noMoreResults = false;
|
||||
@ -94,6 +95,16 @@ Application.Controllers.controller('EventsController', ['$scope', '$state', 'Eve
|
||||
*/
|
||||
$scope.onSingleDay = function (event) { moment(event.start_date).isSame(event.end_date, 'day'); };
|
||||
|
||||
/**
|
||||
* Move down the viewport to the featured event
|
||||
*/
|
||||
$scope.scrollToFeaturedEvent = function () {
|
||||
const card = document.getElementsByClassName('featured-event')[0];
|
||||
if (card) {
|
||||
card.childNodes[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
}
|
||||
|
||||
/* PRIVATE SCOPE */
|
||||
|
||||
/**
|
||||
@ -117,7 +128,8 @@ Application.Controllers.controller('EventsController', ['$scope', '$state', 'Eve
|
||||
});
|
||||
});
|
||||
$scope.eventsGroupByMonth = Object.assign($scope.eventsGroupByMonth, eventsGroupedByMonth);
|
||||
return $scope.monthOrder = Object.keys($scope.eventsGroupByMonth);
|
||||
$scope.monthOrder = Object.keys($scope.eventsGroupByMonth);
|
||||
$scope.featuredEevent = _.minBy(events.filter(e => moment(e.start_date).isAfter()), e => e.start_date);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -21,6 +21,10 @@ Application.Directives.directive('textSetting', ['Setting', 'growl', '_t',
|
||||
if (typeof $scope.type === 'undefined') {
|
||||
$scope.type = 'text';
|
||||
}
|
||||
// 'required' default to true
|
||||
if (typeof $scope.required === 'undefined') {
|
||||
$scope.required = true;
|
||||
}
|
||||
// The setting
|
||||
$scope.setting = {
|
||||
name: $scope.name,
|
||||
|
@ -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);
|
||||
};
|
||||
}]);
|
||||
|
@ -14,6 +14,7 @@ export type mappingType = 'string' | 'text' | 'date' | 'integer' | 'boolean';
|
||||
|
||||
export interface AuthenticationProviderMapping {
|
||||
id?: number,
|
||||
_destroy?: boolean,
|
||||
local_model: 'user' | 'profile',
|
||||
local_field: string,
|
||||
api_field: string,
|
||||
|
@ -4,8 +4,8 @@ export interface ProofOfIdentityFileIndexFilter {
|
||||
}
|
||||
|
||||
export interface ProofOfIdentityFile {
|
||||
id: number,
|
||||
attachment: string,
|
||||
user_id: number,
|
||||
proof_of_identity_file_id: number,
|
||||
id?: number,
|
||||
attachment?: string,
|
||||
user_id?: number,
|
||||
proof_of_identity_type_id: number,
|
||||
}
|
||||
|
@ -121,5 +121,6 @@ export const UserFieldMapping = Object.assign({
|
||||
'profile_attributes.interest': 'profile.interest',
|
||||
'profile_attributes.software_mastered': 'profile.software_mastered',
|
||||
is_allow_contact: 'user.is_allow_contact',
|
||||
is_allow_newsletter: 'user.is_allow_newsletter'
|
||||
is_allow_newsletter: 'user.is_allow_newsletter',
|
||||
group_id: 'user.group_id'
|
||||
}, ...socialMappings);
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user