mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-02-01 21:52:19 +01:00
Merge branch 'family_account' into staging
This commit is contained in:
commit
0dbe1420ac
18
CHANGELOG.md
18
CHANGELOG.md
@ -1,5 +1,23 @@
|
||||
# Changelog Fab-manager
|
||||
|
||||
## v6.0.6 2023 May 4
|
||||
|
||||
- Fix a bug: invalid duration for machine/spaces reservations in statistics, when using slots of not 1 hour
|
||||
- [TODO DEPLOY] `rails fablab:es:build_stats` THEN `rails fablab:maintenance:regenerate_statistics[2014,1]`
|
||||
|
||||
## v6.0.5 2023 May 2
|
||||
|
||||
- Fix a bug: unable to show calendar for Firefox and Safari
|
||||
- Improved error message for event reservation
|
||||
|
||||
## v6.0.4 2023 April 25
|
||||
|
||||
- Fix a bug: notification is broken when delete a project
|
||||
- Fix a bug: broken notifications email
|
||||
- Fix a bug: unable to show calendar
|
||||
- Update translations from Crowdin
|
||||
- [TODO DEPLOY] `rails fablab:maintenance:clean_abuse_notifications`
|
||||
|
||||
## v6.0.3 2023 April 12
|
||||
|
||||
- Fix a bug: unable to install Fab-manager by setup.sh
|
||||
|
@ -269,6 +269,8 @@ GEM
|
||||
net-smtp (0.3.3)
|
||||
net-protocol
|
||||
nio4r (2.5.8)
|
||||
nokogiri (1.14.3-x86_64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.14.3-x86_64-linux)
|
||||
racc (~> 1.4)
|
||||
oauth2 (1.4.4)
|
||||
@ -524,6 +526,7 @@ GEM
|
||||
zeitwerk (2.6.7)
|
||||
|
||||
PLATFORMS
|
||||
x86_64-darwin-20
|
||||
x86_64-linux
|
||||
|
||||
DEPENDENCIES
|
||||
|
69
app/controllers/api/children_controller.rb
Normal file
69
app/controllers/api/children_controller.rb
Normal file
@ -0,0 +1,69 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# API Controller for resources of type Child
|
||||
# Children are used to provide a way to manage multiple users in the family account
|
||||
class API::ChildrenController < API::APIController
|
||||
before_action :authenticate_user!
|
||||
before_action :set_child, only: %i[show update destroy validate]
|
||||
|
||||
def index
|
||||
authorize Child
|
||||
user_id = current_user.id
|
||||
user_id = params[:user_id] if current_user.privileged? && params[:user_id]
|
||||
@children = Child.where(user_id: user_id).includes(:supporting_document_files).order(:created_at)
|
||||
end
|
||||
|
||||
def show
|
||||
authorize @child
|
||||
end
|
||||
|
||||
def create
|
||||
@child = Child.new(child_params)
|
||||
authorize @child
|
||||
if ChildService.create(@child)
|
||||
render status: :created
|
||||
else
|
||||
render json: @child.errors.full_messages, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
authorize @child
|
||||
|
||||
if @child.update(child_params)
|
||||
render status: :ok
|
||||
else
|
||||
render json: @child.errors.full_messages, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize @child
|
||||
@child.destroy
|
||||
head :no_content
|
||||
end
|
||||
|
||||
def validate
|
||||
authorize @child
|
||||
|
||||
cparams = params.require(:child).permit(:validated_at)
|
||||
if ChildService.validate(@child, cparams[:validated_at].present?)
|
||||
render :show, status: :ok, location: child_path(@child)
|
||||
else
|
||||
render json: @child.errors, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_child
|
||||
@child = Child.find(params[:id])
|
||||
end
|
||||
|
||||
def child_params
|
||||
params.require(:child).permit(:first_name, :last_name, :email, :phone, :birthday, :user_id,
|
||||
supporting_document_files_attributes: %i[id supportable_id supportable_type
|
||||
supporting_document_type_id
|
||||
attachment _destroy])
|
||||
end
|
||||
end
|
@ -96,7 +96,7 @@ class API::EventsController < API::APIController
|
||||
# handle general properties
|
||||
event_preparams = params.required(:event).permit(:title, :description, :start_date, :start_time, :end_date, :end_time,
|
||||
:amount, :nb_total_places, :availability_id, :all_day, :recurrence,
|
||||
:recurrence_end_at, :category_id, :event_theme_ids, :age_range_id,
|
||||
:recurrence_end_at, :category_id, :event_theme_ids, :age_range_id, :event_type,
|
||||
event_theme_ids: [],
|
||||
event_image_attributes: %i[id attachment],
|
||||
event_files_attributes: %i[id attachment _destroy],
|
||||
|
@ -72,7 +72,7 @@ class API::PayzenController < API::PaymentsController
|
||||
|
||||
cart = shopping_cart
|
||||
|
||||
if order['answer']['transactions'].any? { |transaction| transaction['status'] == 'PAID' }
|
||||
if order['answer']['transactions'].all? { |transaction| transaction['status'] == 'PAID' }
|
||||
render on_payment_success(params[:order_id], cart)
|
||||
else
|
||||
render json: order['answer'], status: :unprocessable_entity
|
||||
@ -86,10 +86,11 @@ class API::PayzenController < API::PaymentsController
|
||||
|
||||
client = PayZen::Transaction.new
|
||||
transaction = client.get(params[:transaction_uuid])
|
||||
order = PayZen::Order.new.get(params[:order_id])
|
||||
|
||||
cart = shopping_cart
|
||||
|
||||
if transaction['answer']['status'] == 'PAID'
|
||||
if transaction['answer']['status'] == 'PAID' && order['answer']['transactions'].all? { |t| t['status'] == 'PAID' }
|
||||
render on_payment_success(params[:order_id], cart)
|
||||
else
|
||||
render json: transaction['answer'], status: :unprocessable_entity
|
||||
|
@ -48,6 +48,6 @@ class API::SupportingDocumentFilesController < API::APIController
|
||||
|
||||
# Never trust parameters from the scary internet, only allow the white list through.
|
||||
def supporting_document_file_params
|
||||
params.required(:supporting_document_file).permit(:supporting_document_type_id, :attachment, :user_id)
|
||||
params.required(:supporting_document_file).permit(:supporting_document_type_id, :attachment, :supportable_id, :supportable_type)
|
||||
end
|
||||
end
|
||||
|
@ -27,6 +27,7 @@ class API::SupportingDocumentRefusalsController < API::APIController
|
||||
|
||||
# Never trust parameters from the scary internet, only allow the white list through.
|
||||
def supporting_document_refusal_params
|
||||
params.required(:supporting_document_refusal).permit(:message, :operator_id, :user_id, supporting_document_type_ids: [])
|
||||
params.required(:supporting_document_refusal).permit(:message, :operator_id, :supportable_id, :supportable_type,
|
||||
supporting_document_type_ids: [])
|
||||
end
|
||||
end
|
||||
|
@ -45,6 +45,6 @@ class API::SupportingDocumentTypesController < API::APIController
|
||||
end
|
||||
|
||||
def supporting_document_type_params
|
||||
params.require(:supporting_document_type).permit(:name, group_ids: [])
|
||||
params.require(:supporting_document_type).permit(:name, :document_type, group_ids: [])
|
||||
end
|
||||
end
|
||||
|
46
app/frontend/src/javascript/api/child.ts
Normal file
46
app/frontend/src/javascript/api/child.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import apiClient from './clients/api-client';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { Child, ChildIndexFilter } from '../models/child';
|
||||
import ApiLib from '../lib/api';
|
||||
|
||||
export default class ChildAPI {
|
||||
static async index (filters: ChildIndexFilter): Promise<Array<Child>> {
|
||||
const res: AxiosResponse<Array<Child>> = await apiClient.get(`/api/children${ApiLib.filtersToQuery(filters)}`);
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async get (id: number): Promise<Child> {
|
||||
const res: AxiosResponse<Child> = await apiClient.get(`/api/children/${id}`);
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async create (child: Child): Promise<Child> {
|
||||
const data = ApiLib.serializeAttachments(child, 'child', ['supporting_document_files_attributes']);
|
||||
const res: AxiosResponse<Child> = await apiClient.post('/api/children', data, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async update (child: Child): Promise<Child> {
|
||||
const data = ApiLib.serializeAttachments(child, 'child', ['supporting_document_files_attributes']);
|
||||
const res: AxiosResponse<Child> = await apiClient.put(`/api/children/${child.id}`, data, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async destroy (childId: number): Promise<void> {
|
||||
const res: AxiosResponse<void> = await apiClient.delete(`/api/children/${childId}`);
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async validate (child: Child): Promise<Child> {
|
||||
const res: AxiosResponse<Child> = await apiClient.patch(`/api/children/${child.id}/validate`, { child });
|
||||
return res?.data;
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import * as React from 'react';
|
||||
import { SubmitHandler, useFieldArray, useForm, useWatch } from 'react-hook-form';
|
||||
import { Event, EventDecoration, EventPriceCategoryAttributes, RecurrenceOption } from '../../models/event';
|
||||
import { Event, EventDecoration, EventPriceCategoryAttributes, RecurrenceOption, EventType } from '../../models/event';
|
||||
import EventAPI from '../../api/event';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FormInput } from '../form/form-input';
|
||||
@ -40,7 +40,7 @@ interface EventFormProps {
|
||||
* Form to edit or create events
|
||||
*/
|
||||
export const EventForm: React.FC<EventFormProps> = ({ action, event, onError, onSuccess }) => {
|
||||
const { handleSubmit, register, control, setValue, formState } = useForm<Event>({ defaultValues: { ...event } });
|
||||
const { handleSubmit, register, control, setValue, formState } = useForm<Event>({ defaultValues: Object.assign({ event_type: 'standard' }, event) });
|
||||
const output = useWatch<Event>({ control });
|
||||
const { fields, append, remove } = useFieldArray({ control, name: 'event_price_categories_attributes' });
|
||||
|
||||
@ -54,6 +54,7 @@ export const EventForm: React.FC<EventFormProps> = ({ action, event, onError, on
|
||||
const [isOpenRecurrentModal, setIsOpenRecurrentModal] = useState<boolean>(false);
|
||||
const [updatingEvent, setUpdatingEvent] = useState<Event>(null);
|
||||
const [isActiveAccounting, setIsActiveAccounting] = useState<boolean>(false);
|
||||
const [isActiveFamilyAccount, setIsActiveFamilyAccount] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
EventCategoryAPI.index()
|
||||
@ -69,6 +70,7 @@ export const EventForm: React.FC<EventFormProps> = ({ action, event, onError, on
|
||||
.then(data => setPriceCategoriesOptions(data.map(c => decorationToOption(c))))
|
||||
.catch(onError);
|
||||
SettingAPI.get('advanced_accounting').then(res => setIsActiveAccounting(res.value === 'true')).catch(onError);
|
||||
SettingAPI.get('family_account').then(res => setIsActiveFamilyAccount(res.value === 'true')).catch(onError);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@ -168,6 +170,20 @@ export const EventForm: React.FC<EventFormProps> = ({ action, event, onError, on
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* This method provides event type options
|
||||
*/
|
||||
const buildEventTypeOptions = (): Array<SelectOption<EventType>> => {
|
||||
const options = [
|
||||
{ label: t('app.admin.event_form.event_types.standard'), value: 'standard' as EventType },
|
||||
{ label: t('app.admin.event_form.event_types.nominative'), value: 'nominative' as EventType }
|
||||
];
|
||||
if (isActiveFamilyAccount) {
|
||||
options.push({ label: t('app.admin.event_form.event_types.family'), value: 'family' as EventType });
|
||||
}
|
||||
return options;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="event-form">
|
||||
<header>
|
||||
@ -203,6 +219,12 @@ export const EventForm: React.FC<EventFormProps> = ({ action, event, onError, on
|
||||
label={t('app.admin.event_form.description')}
|
||||
limit={null}
|
||||
heading bulletList blockquote link video image />
|
||||
<FormSelect id="event_type"
|
||||
control={control}
|
||||
formState={formState}
|
||||
label={t('app.admin.event_form.event_type')}
|
||||
options={buildEventTypeOptions()}
|
||||
rules={{ required: true }} />
|
||||
<FormSelect id="category_id"
|
||||
control={control}
|
||||
formState={formState}
|
||||
|
@ -0,0 +1,180 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import moment from 'moment';
|
||||
import { Child } from '../../models/child';
|
||||
import { FormInput } from '../form/form-input';
|
||||
import { FabButton } from '../base/fab-button';
|
||||
import { FormFileUpload } from '../form/form-file-upload';
|
||||
import { FileType } from '../../models/file';
|
||||
import { SupportingDocumentType } from '../../models/supporting-document-type';
|
||||
import { User } from '../../models/user';
|
||||
import { SupportingDocumentsRefusalModal } from '../supporting-documents/supporting-documents-refusal-modal';
|
||||
import { FabAlert } from '../base/fab-alert';
|
||||
|
||||
interface ChildFormProps {
|
||||
child: Child;
|
||||
operator: User;
|
||||
onSubmit: (data: Child) => void;
|
||||
supportingDocumentsTypes: Array<SupportingDocumentType>;
|
||||
onSuccess: (message: string) => void,
|
||||
onError: (message: string) => void,
|
||||
}
|
||||
|
||||
/**
|
||||
* A form for creating or editing a child.
|
||||
*/
|
||||
export const ChildForm: React.FC<ChildFormProps> = ({ child, onSubmit, supportingDocumentsTypes, operator, onSuccess, onError }) => {
|
||||
const { t } = useTranslation('public');
|
||||
|
||||
const { register, formState, handleSubmit, setValue, control } = useForm<Child>({
|
||||
defaultValues: child
|
||||
});
|
||||
const output = useWatch<Child>({ control }); // eslint-disable-line
|
||||
const [refuseModalIsOpen, setRefuseModalIsOpen] = useState<boolean>(false);
|
||||
|
||||
/**
|
||||
* get the name of the supporting document type by id
|
||||
*/
|
||||
const getSupportingDocumentsTypeName = (id: number): string => {
|
||||
const supportingDocumentType = supportingDocumentsTypes.find((supportingDocumentType) => supportingDocumentType.id === id);
|
||||
return supportingDocumentType ? supportingDocumentType.name : '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the current operator has administrative rights or is a normal member
|
||||
*/
|
||||
const isPrivileged = (): boolean => {
|
||||
return (operator?.role === 'admin' || operator?.role === 'manager');
|
||||
};
|
||||
|
||||
/**
|
||||
* Open/closes the modal dialog to refuse the documents
|
||||
*/
|
||||
const toggleRefuseModal = (): void => {
|
||||
setRefuseModalIsOpen(!refuseModalIsOpen);
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when the refusal was successfully saved
|
||||
*/
|
||||
const onSaveRefusalSuccess = (message: string): void => {
|
||||
setRefuseModalIsOpen(false);
|
||||
onSuccess(message);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="child-form">
|
||||
{!isPrivileged() &&
|
||||
<FabAlert level='info'>
|
||||
<p>{t('app.public.child_form.child_form_info')}</p>
|
||||
</FabAlert>
|
||||
}
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="grp">
|
||||
<FormInput id="first_name"
|
||||
register={register}
|
||||
rules={{ required: true }}
|
||||
formState={formState}
|
||||
label={t('app.public.child_form.first_name')}
|
||||
/>
|
||||
<FormInput id="last_name"
|
||||
register={register}
|
||||
rules={{ required: true }}
|
||||
formState={formState}
|
||||
label={t('app.public.child_form.last_name')}
|
||||
/>
|
||||
</div>
|
||||
<div className="grp">
|
||||
<FormInput id="birthday"
|
||||
register={register}
|
||||
rules={{ required: true, validate: (value) => moment(value).isAfter(moment().subtract(18, 'year')) }}
|
||||
formState={formState}
|
||||
label={t('app.public.child_form.birthday')}
|
||||
type="date"
|
||||
max={moment().format('YYYY-MM-DD')}
|
||||
min={moment().subtract(18, 'year').format('YYYY-MM-DD')}
|
||||
/>
|
||||
<FormInput id="phone"
|
||||
register={register}
|
||||
formState={formState}
|
||||
label={t('app.public.child_form.phone')}
|
||||
type="tel"
|
||||
/>
|
||||
</div>
|
||||
<FormInput id="email"
|
||||
register={register}
|
||||
formState={formState}
|
||||
label={t('app.public.child_form.email')}
|
||||
/>
|
||||
|
||||
{!isPrivileged() && <>
|
||||
<h3 className="missing-file">{t('app.public.child_form.supporting_documents')}</h3>
|
||||
{output.supporting_document_files_attributes.map((sf, index) => {
|
||||
return (
|
||||
<FormFileUpload key={index}
|
||||
defaultFile={sf as FileType}
|
||||
id={`supporting_document_files_attributes.${index}`}
|
||||
accept="application/pdf"
|
||||
setValue={setValue}
|
||||
label={getSupportingDocumentsTypeName(sf.supporting_document_type_id)}
|
||||
showRemoveButton={false}
|
||||
register={register}
|
||||
formState={formState} />
|
||||
);
|
||||
})}
|
||||
</>}
|
||||
|
||||
<div className="actions">
|
||||
<FabButton type="button" className='is-secondary' onClick={handleSubmit(onSubmit)}>
|
||||
{t('app.public.child_form.save')}
|
||||
</FabButton>
|
||||
</div>
|
||||
|
||||
{isPrivileged() && <>
|
||||
<h3 className="missing-file">{t('app.public.child_form.supporting_documents')}</h3>
|
||||
<div className="document-list">
|
||||
{output.supporting_document_files_attributes.map((sf, index) => {
|
||||
return (
|
||||
<div key={index} className="document-list-item">
|
||||
<span className="type">{getSupportingDocumentsTypeName(sf.supporting_document_type_id)}</span>
|
||||
{sf.attachment_url && (
|
||||
<div className='file'>
|
||||
<p>{sf.attachment}</p>
|
||||
<a href={sf.attachment_url} target="_blank" rel="noreferrer" className='fab-button is-black'>
|
||||
<span className="fab-button--icon-only"><i className="fas fa-eye"></i></span>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{!sf.attachment_url && (
|
||||
<div className="missing">
|
||||
<p>{t('app.public.child_form.to_complete')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>}
|
||||
|
||||
{isPrivileged() && <>
|
||||
<FabAlert level='info'>
|
||||
<p>{t('app.public.child_form.refuse_documents_info')}</p>
|
||||
</FabAlert>
|
||||
<div className="actions">
|
||||
<FabButton className="refuse-btn is-secondary" onClick={toggleRefuseModal}>{t('app.public.child_form.refuse_documents')}</FabButton>
|
||||
<SupportingDocumentsRefusalModal
|
||||
isOpen={refuseModalIsOpen}
|
||||
proofOfIdentityTypes={supportingDocumentsTypes}
|
||||
toggleModal={toggleRefuseModal}
|
||||
operator={operator}
|
||||
supportable={child}
|
||||
documentType="Child"
|
||||
onError={onError}
|
||||
onSuccess={onSaveRefusalSuccess} />
|
||||
</div>
|
||||
</>}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
import { Child } from '../../models/child';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FabButton } from '../base/fab-button';
|
||||
import FormatLib from '../../lib/format';
|
||||
import { DeleteChildModal } from './delete-child-modal';
|
||||
import ChildAPI from '../../api/child';
|
||||
import { PencilSimple, Trash, UserSquare } from 'phosphor-react';
|
||||
|
||||
interface ChildItemProps {
|
||||
child: Child;
|
||||
size: 'sm' | 'lg';
|
||||
onEdit: (child: Child) => void;
|
||||
onDelete: (child: Child, error: string) => void;
|
||||
onError: (error: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A child item.
|
||||
*/
|
||||
export const ChildItem: React.FC<ChildItemProps> = ({ child, size, onEdit, onDelete, onError }) => {
|
||||
const { t } = useTranslation('public');
|
||||
const [isOpenDeleteChildModal, setIsOpenDeleteChildModal] = React.useState<boolean>(false);
|
||||
|
||||
/**
|
||||
* Toggle the delete child modal
|
||||
*/
|
||||
const toggleDeleteChildModal = () => setIsOpenDeleteChildModal(!isOpenDeleteChildModal);
|
||||
|
||||
/**
|
||||
* Delete a child
|
||||
*/
|
||||
const deleteChild = () => {
|
||||
ChildAPI.destroy(child.id).then(() => {
|
||||
toggleDeleteChildModal();
|
||||
onDelete(child, t('app.public.child_item.deleted'));
|
||||
}).catch((e) => {
|
||||
console.error(e);
|
||||
onError(t('app.public.child_item.unable_to_delete'));
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`child-item ${size} ${child.validated_at ? 'is-validated' : ''}`}>
|
||||
<div className='status'>
|
||||
<UserSquare size={24} weight="light" />
|
||||
</div>
|
||||
<div>
|
||||
<span>{t('app.public.child_item.last_name')}</span>
|
||||
<p>{child.last_name}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span>{t('app.public.child_item.first_name')}</span>
|
||||
<p>{child.first_name}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span>{t('app.public.child_item.birthday')}</span>
|
||||
<p>{FormatLib.date(child.birthday)}</p>
|
||||
</div>
|
||||
<div className="actions edit-destroy-buttons">
|
||||
<FabButton onClick={() => onEdit(child)} className="edit-btn">
|
||||
<PencilSimple size={20} weight="fill" />
|
||||
</FabButton>
|
||||
<FabButton onClick={toggleDeleteChildModal} className="delete-btn">
|
||||
<Trash size={20} weight="fill" />
|
||||
</FabButton>
|
||||
<DeleteChildModal isOpen={isOpenDeleteChildModal} toggleModal={toggleDeleteChildModal} child={child} onDelete={deleteChild} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,64 @@
|
||||
import * as React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FabModal, ModalSize } from '../base/fab-modal';
|
||||
import { Child } from '../../models/child';
|
||||
import ChildAPI from '../../api/child';
|
||||
import { ChildForm } from './child-form';
|
||||
import { SupportingDocumentType } from '../../models/supporting-document-type';
|
||||
import { ChildValidation } from './child-validation';
|
||||
import { User } from '../../models/user';
|
||||
|
||||
interface ChildModalProps {
|
||||
child?: Child;
|
||||
operator: User;
|
||||
isOpen: boolean;
|
||||
toggleModal: () => void;
|
||||
onSuccess: (child: Child, msg: string) => void;
|
||||
onError: (error: string) => void;
|
||||
supportingDocumentsTypes: Array<SupportingDocumentType>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A modal for creating or editing a child.
|
||||
*/
|
||||
export const ChildModal: React.FC<ChildModalProps> = ({ child, isOpen, toggleModal, onSuccess, onError, supportingDocumentsTypes, operator }) => {
|
||||
const { t } = useTranslation('public');
|
||||
|
||||
/**
|
||||
* Save the child to the API
|
||||
*/
|
||||
const handleSaveChild = async (data: Child): Promise<void> => {
|
||||
try {
|
||||
if (child?.id) {
|
||||
await ChildAPI.update(data);
|
||||
} else {
|
||||
await ChildAPI.create(data);
|
||||
}
|
||||
toggleModal();
|
||||
onSuccess(data, '');
|
||||
} catch (error) {
|
||||
onError(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FabModal title={t(`app.public.child_modal.${child?.id ? 'edit' : 'new'}_child`)}
|
||||
width={ModalSize.large}
|
||||
isOpen={isOpen}
|
||||
toggleModal={toggleModal}
|
||||
closeButton={true}
|
||||
confirmButton={false} >
|
||||
{(operator?.role === 'admin' || operator?.role === 'manager') &&
|
||||
<ChildValidation child={child} onSuccess={onSuccess} onError={onError} />
|
||||
}
|
||||
<ChildForm
|
||||
child={child}
|
||||
onSubmit={handleSaveChild}
|
||||
supportingDocumentsTypes={supportingDocumentsTypes}
|
||||
operator={operator}
|
||||
onSuccess={onSuccess}
|
||||
onError={onError}
|
||||
/>
|
||||
</FabModal>
|
||||
);
|
||||
};
|
@ -0,0 +1,54 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import * as React from 'react';
|
||||
import Switch from 'react-switch';
|
||||
import _ from 'lodash';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Child } from '../../models/child';
|
||||
import ChildAPI from '../../api/child';
|
||||
import { TDateISO } from '../../typings/date-iso';
|
||||
|
||||
interface ChildValidationProps {
|
||||
child: Child
|
||||
onSuccess: (message: string) => void,
|
||||
onError: (message: string) => void,
|
||||
}
|
||||
|
||||
/**
|
||||
* This component allows to configure boolean value for a setting.
|
||||
*/
|
||||
export const ChildValidation: React.FC<ChildValidationProps> = ({ child, onSuccess, onError }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
const [value, setValue] = useState<boolean>(!!(child?.validated_at));
|
||||
|
||||
useEffect(() => {
|
||||
setValue(!!(child?.validated_at));
|
||||
}, [child]);
|
||||
|
||||
/**
|
||||
* Callback triggered when the 'switch' is changed.
|
||||
*/
|
||||
const handleChanged = (_value: boolean) => {
|
||||
setValue(_value);
|
||||
const _child = _.clone(child);
|
||||
if (_value) {
|
||||
_child.validated_at = new Date().toISOString() as TDateISO;
|
||||
} else {
|
||||
_child.validated_at = null;
|
||||
}
|
||||
ChildAPI.validate(_child)
|
||||
.then(() => {
|
||||
onSuccess(t(`app.admin.child_validation.${_value ? 'validate' : 'invalidate'}_child_success`));
|
||||
}).catch(err => {
|
||||
setValue(!_value);
|
||||
onError(t(`app.admin.child_validation.${_value ? 'validate' : 'invalidate'}_child_error`) + err);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="child-validation">
|
||||
<label htmlFor="child-validation-switch">{t('app.admin.child_validation.validate_child')}</label>
|
||||
<Switch checked={value} id="child-validation-switch" onChange={handleChanged} className="switch"></Switch>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,129 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { react2angular } from 'react2angular';
|
||||
import { Child } from '../../models/child';
|
||||
import ChildAPI from '../../api/child';
|
||||
import { User } from '../../models/user';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Loader } from '../base/loader';
|
||||
import { IApplication } from '../../models/application';
|
||||
import { ChildModal } from './child-modal';
|
||||
import { ChildItem } from './child-item';
|
||||
import { FabButton } from '../base/fab-button';
|
||||
import { SupportingDocumentType } from '../../models/supporting-document-type';
|
||||
import SupportingDocumentTypeAPI from '../../api/supporting-document-type';
|
||||
|
||||
declare const Application: IApplication;
|
||||
|
||||
interface ChildrenDashboardProps {
|
||||
user: User;
|
||||
operator: User;
|
||||
adminPanel?: boolean;
|
||||
onSuccess: (error: string) => void;
|
||||
onError: (error: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of children belonging to the current user.
|
||||
*/
|
||||
export const ChildrenDashboard: React.FC<ChildrenDashboardProps> = ({ user, operator, adminPanel, onError, onSuccess }) => {
|
||||
const { t } = useTranslation('public');
|
||||
|
||||
const [children, setChildren] = useState<Array<Child>>([]);
|
||||
const [isOpenChildModal, setIsOpenChildModal] = useState<boolean>(false);
|
||||
const [child, setChild] = useState<Child>();
|
||||
const [supportingDocumentsTypes, setSupportingDocumentsTypes] = useState<Array<SupportingDocumentType>>([]);
|
||||
|
||||
useEffect(() => {
|
||||
ChildAPI.index({ user_id: user.id }).then(setChildren);
|
||||
SupportingDocumentTypeAPI.index({ document_type: 'Child' }).then(tData => {
|
||||
setSupportingDocumentsTypes(tData);
|
||||
});
|
||||
}, [user]);
|
||||
|
||||
/**
|
||||
* Open the add child modal
|
||||
*/
|
||||
const addChild = () => {
|
||||
setIsOpenChildModal(true);
|
||||
setChild({
|
||||
user_id: user.id,
|
||||
supporting_document_files_attributes: supportingDocumentsTypes.map(t => {
|
||||
return { supporting_document_type_id: t.id };
|
||||
})
|
||||
} as Child);
|
||||
};
|
||||
|
||||
/**
|
||||
* Open the edit child modal
|
||||
*/
|
||||
const editChild = (child: Child) => {
|
||||
setIsOpenChildModal(true);
|
||||
setChild({
|
||||
...child,
|
||||
supporting_document_files_attributes: supportingDocumentsTypes.map(t => {
|
||||
const file = child.supporting_document_files_attributes.find(f => f.supporting_document_type_id === t.id);
|
||||
return file || { supporting_document_type_id: t.id };
|
||||
})
|
||||
} as Child);
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete a child
|
||||
*/
|
||||
const handleDeleteChildSuccess = (_child: Child, msg: string) => {
|
||||
ChildAPI.index({ user_id: user.id }).then(setChildren);
|
||||
onSuccess(msg);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle save child success from the API
|
||||
*/
|
||||
const handleSaveChildSuccess = (_data: Child, msg: string) => {
|
||||
ChildAPI.index({ user_id: user.id }).then(setChildren);
|
||||
if (msg) {
|
||||
onSuccess(msg);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the current operator has administrative rights or is a normal member
|
||||
*/
|
||||
const isPrivileged = (): boolean => {
|
||||
return (operator?.role === 'admin' || operator?.role === 'manager');
|
||||
};
|
||||
|
||||
return (
|
||||
<section className='children-dashboard'>
|
||||
<header>
|
||||
{adminPanel
|
||||
? <h2>{t('app.public.children_dashboard.heading')}</h2>
|
||||
: <h2>{t('app.public.children_dashboard.member_heading')}</h2>
|
||||
}
|
||||
{!isPrivileged() && (
|
||||
<div className="grpBtn">
|
||||
<FabButton className="main-action-btn" onClick={addChild}>
|
||||
{t('app.public.children_dashboard.add_child')}
|
||||
</FabButton>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
|
||||
<div className="children-list">
|
||||
{children.map(child => (
|
||||
<ChildItem key={child.id} child={child} size='lg' onEdit={editChild} onDelete={handleDeleteChildSuccess} onError={onError} />
|
||||
))}
|
||||
</div>
|
||||
<ChildModal child={child} isOpen={isOpenChildModal} toggleModal={() => setIsOpenChildModal(false)} onSuccess={handleSaveChildSuccess} onError={onError} supportingDocumentsTypes={supportingDocumentsTypes} operator={operator} />
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
const ChildrenDashboardWrapper: React.FC<ChildrenDashboardProps> = (props) => {
|
||||
return (
|
||||
<Loader>
|
||||
<ChildrenDashboard {...props} />
|
||||
</Loader>
|
||||
);
|
||||
};
|
||||
|
||||
Application.Components.component('childrenDashboard', react2angular(ChildrenDashboardWrapper, ['user', 'operator', 'adminPanel', 'onSuccess', 'onError']));
|
@ -0,0 +1,37 @@
|
||||
import * as React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FabModal } from '../base/fab-modal';
|
||||
import { Child } from '../../models/child';
|
||||
|
||||
interface DeleteChildModalProps {
|
||||
isOpen: boolean,
|
||||
toggleModal: () => void,
|
||||
child: Child,
|
||||
onDelete: (child: Child) => void,
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal dialog to remove a requested child
|
||||
*/
|
||||
export const DeleteChildModal: React.FC<DeleteChildModalProps> = ({ isOpen, toggleModal, onDelete, child }) => {
|
||||
const { t } = useTranslation('public');
|
||||
|
||||
/**
|
||||
* Callback triggered when the child confirms the deletion
|
||||
*/
|
||||
const handleDeleteChild = () => {
|
||||
onDelete(child);
|
||||
};
|
||||
|
||||
return (
|
||||
<FabModal title={t('app.public.delete_child_modal.confirmation_required')}
|
||||
isOpen={isOpen}
|
||||
toggleModal={toggleModal}
|
||||
closeButton={true}
|
||||
confirmButton={t('app.public.delete_child_modal.confirm')}
|
||||
onConfirm={handleDeleteChild}
|
||||
className="delete-child-modal">
|
||||
<p>{t('app.public.delete_child_modal.confirm_delete_child')}</p>
|
||||
</FabModal>
|
||||
);
|
||||
};
|
@ -19,12 +19,13 @@ type FormFileUploadProps<TFieldValues> = FormComponent<TFieldValues> & AbstractF
|
||||
accept?: string,
|
||||
onFileChange?: (value: FileType) => void,
|
||||
onFileRemove?: () => void,
|
||||
showRemoveButton?: boolean,
|
||||
}
|
||||
|
||||
/**
|
||||
* This component allows to upload file, in forms managed by react-hook-form.
|
||||
*/
|
||||
export const FormFileUpload = <TFieldValues extends FieldValues>({ id, label, register, defaultFile, className, rules, disabled, error, warning, formState, onFileChange, onFileRemove, accept, setValue }: FormFileUploadProps<TFieldValues>) => {
|
||||
export const FormFileUpload = <TFieldValues extends FieldValues>({ id, label, register, defaultFile, className, rules, disabled, error, warning, formState, onFileChange, onFileRemove, accept, setValue, showRemoveButton = true }: FormFileUploadProps<TFieldValues>) => {
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
const [file, setFile] = useState<FileType>(defaultFile);
|
||||
@ -74,9 +75,10 @@ export const FormFileUpload = <TFieldValues extends FieldValues>({ id, label, re
|
||||
|
||||
return (
|
||||
<div className={`form-file-upload ${label ? 'with-label' : ''} ${classNames}`}>
|
||||
{hasFile() && (
|
||||
<span>{file.attachment_name}</span>
|
||||
)}
|
||||
{hasFile()
|
||||
? <span>{file.attachment_name}</span>
|
||||
: <span className='placeholder'>{t('app.shared.form_file_upload.placeholder')}</span>
|
||||
}
|
||||
<div className="actions">
|
||||
{file?.id && file?.attachment_url && (
|
||||
<a href={file.attachment_url}
|
||||
@ -100,7 +102,7 @@ export const FormFileUpload = <TFieldValues extends FieldValues>({ id, label, re
|
||||
id={`${id}[attachment_files]`}
|
||||
onChange={onFileSelected}
|
||||
placeholder={placeholder()}/>
|
||||
{hasFile() &&
|
||||
{showRemoveButton && hasFile() &&
|
||||
<FabButton onClick={onRemoveFile} icon={<Trash size={20} weight="fill" />} className="is-main" />
|
||||
}
|
||||
</div>
|
||||
|
@ -22,13 +22,15 @@ type FormInputProps<TFieldValues, TInputType> = FormComponent<TFieldValues> & Ab
|
||||
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void,
|
||||
nullable?: boolean,
|
||||
ariaLabel?: string,
|
||||
maxLength?: number
|
||||
maxLength?: number,
|
||||
max?: number | string,
|
||||
min?: number | string,
|
||||
}
|
||||
|
||||
/**
|
||||
* This component is a template for an input component to use within React Hook Form
|
||||
*/
|
||||
export const FormInput = <TFieldValues extends FieldValues, TInputType>({ id, register, label, tooltip, defaultValue, icon, className, rules, disabled, type, addOn, addOnAction, addOnClassName, addOnAriaLabel, placeholder, error, warning, formState, step, onChange, debounce, accept, nullable = false, ariaLabel, maxLength }: FormInputProps<TFieldValues, TInputType>) => {
|
||||
export const FormInput = <TFieldValues extends FieldValues, TInputType>({ id, register, label, tooltip, defaultValue, icon, className, rules, disabled, type, addOn, addOnAction, addOnClassName, addOnAriaLabel, placeholder, error, warning, formState, step, onChange, debounce, accept, nullable = false, ariaLabel, maxLength, max, min }: FormInputProps<TFieldValues, TInputType>) => {
|
||||
const [characterCount, setCharacterCount] = useState<number>(0);
|
||||
|
||||
/**
|
||||
@ -100,7 +102,9 @@ export const FormInput = <TFieldValues extends FieldValues, TInputType>({ id, re
|
||||
disabled={typeof disabled === 'function' ? disabled(id) : disabled}
|
||||
placeholder={placeholder}
|
||||
accept={accept}
|
||||
maxLength={maxLength} />
|
||||
maxLength={maxLength}
|
||||
max={max}
|
||||
min={min}/>
|
||||
{(type === 'file' && placeholder) && <span className='fab-button is-black file-placeholder'>{placeholder}</span>}
|
||||
{maxLength && <span className='countdown'>{characterCount} / {maxLength}</span>}
|
||||
{addOn && addOnAction && <button aria-label={addOnAriaLabel} type="button" onClick={addOnAction} className={`addon ${addOnClassName || ''} is-btn`}>{addOn}</button>}
|
||||
|
@ -54,7 +54,7 @@ export const PaymentScheduleItemActions: React.FC<PaymentScheduleItemActionsProp
|
||||
* Check if the current operator has administrative rights or is a normal member
|
||||
*/
|
||||
const isPrivileged = (): boolean => {
|
||||
return (operator.role === 'admin' || operator.role === 'manager');
|
||||
return (operator?.role === 'admin' || operator?.role === 'manager');
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -75,7 +75,7 @@ export const PayzenForm: React.FC<PayzenFormProps> = ({ onSubmit, onSuccess, onE
|
||||
if (updateCard) return onSuccess(null);
|
||||
|
||||
const transaction = event.clientAnswer.transactions[0];
|
||||
if (event.clientAnswer.orderStatus === 'PAID') {
|
||||
if (event.clientAnswer.orderStatus === 'PAID' && transaction?.status === 'PAID') {
|
||||
confirmPayment(event, transaction).then((confirmation) => {
|
||||
PayZenKR.current.removeForms().then(() => {
|
||||
onSuccess(confirmation);
|
||||
|
@ -49,7 +49,7 @@ export const SupportingDocumentsFiles: React.FC<SupportingDocumentsFilesProps> =
|
||||
SupportingDocumentTypeAPI.index({ group_id: currentUser.group_id }).then(tData => {
|
||||
setSupportingDocumentsTypes(tData);
|
||||
});
|
||||
SupportingDocumentFileAPI.index({ user_id: currentUser.id }).then(fData => {
|
||||
SupportingDocumentFileAPI.index({ supportable_id: currentUser.id, supportable_type: 'User' }).then(fData => {
|
||||
setSupportingDocumentsFiles(fData);
|
||||
});
|
||||
}, []);
|
||||
@ -106,7 +106,8 @@ export const SupportingDocumentsFiles: React.FC<SupportingDocumentsFilesProps> =
|
||||
for (const proofOfIdentityTypeId of Object.keys(files)) {
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append('supporting_document_file[user_id]', currentUser.id.toString());
|
||||
formData.append('supporting_document_file[supportable_id]', currentUser.id.toString());
|
||||
formData.append('supporting_document_file[supportable_type]', 'User');
|
||||
formData.append('supporting_document_file[supporting_document_type_id]', proofOfIdentityTypeId);
|
||||
formData.append('supporting_document_file[attachment]', files[proofOfIdentityTypeId]);
|
||||
const proofOfIdentityFile = getSupportingDocumentsFileByType(parseInt(proofOfIdentityTypeId, 10));
|
||||
@ -117,7 +118,7 @@ export const SupportingDocumentsFiles: React.FC<SupportingDocumentsFilesProps> =
|
||||
}
|
||||
}
|
||||
if (Object.keys(files).length > 0) {
|
||||
SupportingDocumentFileAPI.index({ user_id: currentUser.id }).then(fData => {
|
||||
SupportingDocumentFileAPI.index({ supportable_id: currentUser.id, supportable_type: 'User' }).then(fData => {
|
||||
setSupportingDocumentsFiles(fData);
|
||||
setFiles({});
|
||||
onSuccess(t('app.logged.dashboard.supporting_documents_files.file_successfully_uploaded'));
|
||||
|
@ -5,6 +5,7 @@ import { FabModal } from '../base/fab-modal';
|
||||
import { SupportingDocumentType } from '../../models/supporting-document-type';
|
||||
import { SupportingDocumentRefusal } from '../../models/supporting-document-refusal';
|
||||
import { User } from '../../models/user';
|
||||
import { Child } from '../../models/child';
|
||||
import SupportingDocumentRefusalAPI from '../../api/supporting-document-refusal';
|
||||
import { SupportingDocumentsRefusalForm } from './supporting-documents-refusal-form';
|
||||
|
||||
@ -15,19 +16,21 @@ interface SupportingDocumentsRefusalModalProps {
|
||||
onError: (message: string) => void,
|
||||
proofOfIdentityTypes: Array<SupportingDocumentType>,
|
||||
operator: User,
|
||||
member: User
|
||||
supportable: User | Child,
|
||||
documentType: 'User' | 'Child',
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal dialog to notify the member that his documents are refused
|
||||
*/
|
||||
export const SupportingDocumentsRefusalModal: React.FC<SupportingDocumentsRefusalModalProps> = ({ isOpen, toggleModal, onSuccess, proofOfIdentityTypes, operator, member, onError }) => {
|
||||
export const SupportingDocumentsRefusalModal: React.FC<SupportingDocumentsRefusalModalProps> = ({ isOpen, toggleModal, onSuccess, proofOfIdentityTypes, operator, supportable, onError, documentType }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
const [data, setData] = useState<SupportingDocumentRefusal>({
|
||||
id: null,
|
||||
operator_id: operator.id,
|
||||
user_id: member.id,
|
||||
supportable_id: supportable.id,
|
||||
supportable_type: documentType,
|
||||
supporting_document_type_ids: [],
|
||||
message: ''
|
||||
});
|
||||
|
@ -63,6 +63,7 @@ export const SupportingDocumentsTypeForm: React.FC<SupportingDocumentsTypeFormPr
|
||||
{t('app.admin.settings.account.supporting_documents_type_form.type_form_info')}
|
||||
</div>
|
||||
<form name="supportingDocumentTypeForm">
|
||||
{supportingDocumentType?.document_type === 'User' &&
|
||||
<div className="field">
|
||||
<Select defaultValue={groupsValues()}
|
||||
placeholder={t('app.admin.settings.account.supporting_documents_type_form.select_group')}
|
||||
@ -70,6 +71,7 @@ export const SupportingDocumentsTypeForm: React.FC<SupportingDocumentsTypeFormPr
|
||||
options={buildOptions()}
|
||||
isMulti />
|
||||
</div>
|
||||
}
|
||||
<div className="field">
|
||||
<FabInput id="supporting_document_type_name"
|
||||
icon={<i className="fa fa-edit" />}
|
||||
|
@ -14,18 +14,19 @@ interface SupportingDocumentsTypeModalProps {
|
||||
onError: (message: string) => void,
|
||||
groups: Array<Group>,
|
||||
proofOfIdentityType?: SupportingDocumentType,
|
||||
documentType: 'User' | 'Child',
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal dialog to create/edit a supporting documents type
|
||||
*/
|
||||
export const SupportingDocumentsTypeModal: React.FC<SupportingDocumentsTypeModalProps> = ({ isOpen, toggleModal, onSuccess, onError, proofOfIdentityType, groups }) => {
|
||||
export const SupportingDocumentsTypeModal: React.FC<SupportingDocumentsTypeModalProps> = ({ isOpen, toggleModal, onSuccess, onError, proofOfIdentityType, groups, documentType }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
const [data, setData] = useState<SupportingDocumentType>({ id: proofOfIdentityType?.id, group_ids: proofOfIdentityType?.group_ids || [], name: proofOfIdentityType?.name || '' });
|
||||
const [data, setData] = useState<SupportingDocumentType>({ id: proofOfIdentityType?.id, group_ids: proofOfIdentityType?.group_ids || [], name: proofOfIdentityType?.name || '', document_type: documentType });
|
||||
|
||||
useEffect(() => {
|
||||
setData({ id: proofOfIdentityType?.id, group_ids: proofOfIdentityType?.group_ids || [], name: proofOfIdentityType?.name || '' });
|
||||
setData({ id: proofOfIdentityType?.id, group_ids: proofOfIdentityType?.group_ids || [], name: proofOfIdentityType?.name || '', document_type: documentType });
|
||||
}, [proofOfIdentityType]);
|
||||
|
||||
/**
|
||||
@ -63,7 +64,7 @@ export const SupportingDocumentsTypeModal: React.FC<SupportingDocumentsTypeModal
|
||||
* Check if the form is valid (not empty)
|
||||
*/
|
||||
const isPreventedSaveType = (): boolean => {
|
||||
return !data.name || data.group_ids.length === 0;
|
||||
return !data.name || (documentType === 'User' && data.group_ids.length === 0);
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -15,18 +15,20 @@ import SupportingDocumentTypeAPI from '../../api/supporting-document-type';
|
||||
import { FabPanel } from '../base/fab-panel';
|
||||
import { FabAlert } from '../base/fab-alert';
|
||||
import { FabButton } from '../base/fab-button';
|
||||
import { PencilSimple, Trash } from 'phosphor-react';
|
||||
|
||||
declare const Application: IApplication;
|
||||
|
||||
interface SupportingDocumentsTypesListProps {
|
||||
onSuccess: (message: string) => void,
|
||||
onError: (message: string) => void,
|
||||
documentType: 'User' | 'Child',
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 SupportingDocumentsTypesList: React.FC<SupportingDocumentsTypesListProps> = ({ onSuccess, onError, documentType }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
// list of displayed supporting documents type
|
||||
@ -48,7 +50,7 @@ const SupportingDocumentsTypesList: React.FC<SupportingDocumentsTypesListProps>
|
||||
useEffect(() => {
|
||||
GroupAPI.index({ disabled: false }).then(data => {
|
||||
setGroups(data);
|
||||
SupportingDocumentTypeAPI.index().then(pData => {
|
||||
SupportingDocumentTypeAPI.index({ document_type: documentType }).then(pData => {
|
||||
setSupportingDocumentsTypes(pData);
|
||||
});
|
||||
});
|
||||
@ -91,7 +93,7 @@ const SupportingDocumentsTypesList: React.FC<SupportingDocumentsTypesListProps>
|
||||
*/
|
||||
const onSaveTypeSuccess = (message: string): void => {
|
||||
setModalIsOpen(false);
|
||||
SupportingDocumentTypeAPI.index().then(pData => {
|
||||
SupportingDocumentTypeAPI.index({ document_type: documentType }).then(pData => {
|
||||
setSupportingDocumentsTypes(orderTypes(pData, supportingDocumentsTypeOrder));
|
||||
onSuccess(message);
|
||||
}).catch((error) => {
|
||||
@ -121,7 +123,7 @@ const SupportingDocumentsTypesList: React.FC<SupportingDocumentsTypesListProps>
|
||||
*/
|
||||
const onDestroySuccess = (message: string): void => {
|
||||
setDestroyModalIsOpen(false);
|
||||
SupportingDocumentTypeAPI.index().then(pData => {
|
||||
SupportingDocumentTypeAPI.index({ document_type: documentType }).then(pData => {
|
||||
setSupportingDocumentsTypes(pData);
|
||||
setSupportingDocumentsTypes(orderTypes(pData, supportingDocumentsTypeOrder));
|
||||
onSuccess(message);
|
||||
@ -190,6 +192,7 @@ const SupportingDocumentsTypesList: React.FC<SupportingDocumentsTypesListProps>
|
||||
window.location.href = '/#!/admin/members?tabs=1';
|
||||
};
|
||||
|
||||
if (documentType === 'User') {
|
||||
return (
|
||||
<FabPanel className="supporting-documents-types-list" header={<div>
|
||||
<span>{t('app.admin.settings.account.supporting_documents_types_list.add_supporting_documents_types')}</span>
|
||||
@ -211,6 +214,7 @@ const SupportingDocumentsTypesList: React.FC<SupportingDocumentsTypesListProps>
|
||||
<SupportingDocumentsTypeModal isOpen={modalIsOpen}
|
||||
groups={groups}
|
||||
proofOfIdentityType={supportingDocumentsType}
|
||||
documentType={documentType}
|
||||
toggleModal={toggleCreateAndEditModal}
|
||||
onSuccess={onSaveTypeSuccess}
|
||||
onError={onError} />
|
||||
@ -245,12 +249,12 @@ const SupportingDocumentsTypesList: React.FC<SupportingDocumentsTypesListProps>
|
||||
<td>{getGroupsNames(poit.group_ids)}</td>
|
||||
<td>{poit.name}</td>
|
||||
<td>
|
||||
<div className="buttons">
|
||||
<div className="edit-destroy-buttons">
|
||||
<FabButton className="edit-btn" onClick={editType(poit)}>
|
||||
<i className="fa fa-edit" />
|
||||
<PencilSimple size={20} weight="fill" />
|
||||
</FabButton>
|
||||
<FabButton className="delete-btn" onClick={destroyType(poit.id)}>
|
||||
<i className="fa fa-trash" />
|
||||
<Trash size={20} weight="fill" />
|
||||
</FabButton>
|
||||
</div>
|
||||
</td>
|
||||
@ -267,6 +271,59 @@ const SupportingDocumentsTypesList: React.FC<SupportingDocumentsTypesListProps>
|
||||
</div>
|
||||
</FabPanel>
|
||||
);
|
||||
} else if (documentType === 'Child') {
|
||||
return (
|
||||
<div className="supporting-documents-types-list">
|
||||
<div className="types-list">
|
||||
<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}
|
||||
documentType={documentType}
|
||||
toggleModal={toggleCreateAndEditModal}
|
||||
onSuccess={onSaveTypeSuccess}
|
||||
onError={onError} />
|
||||
<DeleteSupportingDocumentsTypeModal isOpen={destroyModalIsOpen}
|
||||
proofOfIdentityTypeId={supportingDocumentsTypeId}
|
||||
toggleModal={toggleDestroyModal}
|
||||
onSuccess={onDestroySuccess}
|
||||
onError={onError}/>
|
||||
|
||||
<div className="document-list">
|
||||
{supportingDocumentsTypes.map(poit => {
|
||||
return (
|
||||
<div key={poit.id} className="document-list-item">
|
||||
<div className='file'>
|
||||
<p>{poit.name}</p>
|
||||
<div className="edit-destroy-buttons">
|
||||
<FabButton className="edit-btn" onClick={editType(poit)}>
|
||||
<PencilSimple size={20} weight="fill" />
|
||||
</FabButton>
|
||||
<FabButton className="delete-btn" onClick={destroyType(poit.id)}>
|
||||
<Trash size={20} weight="fill" />
|
||||
</FabButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{!hasTypes() && (
|
||||
<p className="no-types-info">
|
||||
<HtmlTranslate trKey="app.admin.settings.account.supporting_documents_types_list.no_types" />
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const SupportingDocumentsTypesListWrapper: React.FC<SupportingDocumentsTypesListProps> = (props) => {
|
||||
@ -277,4 +334,4 @@ const SupportingDocumentsTypesListWrapper: React.FC<SupportingDocumentsTypesList
|
||||
);
|
||||
};
|
||||
|
||||
Application.Components.component('supportingDocumentsTypesList', react2angular(SupportingDocumentsTypesListWrapper, ['onSuccess', 'onError']));
|
||||
Application.Components.component('supportingDocumentsTypesList', react2angular(SupportingDocumentsTypesListWrapper, ['onSuccess', 'onError', 'documentType']));
|
||||
|
@ -19,6 +19,7 @@ declare const Application: IApplication;
|
||||
interface SupportingDocumentsValidationProps {
|
||||
operator: User,
|
||||
member: User
|
||||
documentType: 'User' | 'Child',
|
||||
onSuccess: (message: string) => void,
|
||||
onError: (message: string) => void,
|
||||
}
|
||||
@ -26,7 +27,7 @@ interface SupportingDocumentsValidationProps {
|
||||
/**
|
||||
* 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 SupportingDocumentsValidation: React.FC<SupportingDocumentsValidationProps> = ({ operator, member, onSuccess, onError, documentType }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
// list of supporting documents type
|
||||
@ -39,7 +40,7 @@ const SupportingDocumentsValidation: React.FC<SupportingDocumentsValidationProps
|
||||
SupportingDocumentTypeAPI.index({ group_id: member.group_id }).then(tData => {
|
||||
setDocumentsTypes(tData);
|
||||
});
|
||||
SupportingDocumentFileAPI.index({ user_id: member.id }).then(fData => {
|
||||
SupportingDocumentFileAPI.index({ supportable_id: member.id, supportable_type: 'User' }).then(fData => {
|
||||
setDocumentsFiles(fData);
|
||||
});
|
||||
}, []);
|
||||
@ -112,7 +113,8 @@ const SupportingDocumentsValidation: React.FC<SupportingDocumentsValidationProps
|
||||
proofOfIdentityTypes={documentsTypes}
|
||||
toggleModal={toggleModal}
|
||||
operator={operator}
|
||||
member={member}
|
||||
supportable={member}
|
||||
documentType={documentType}
|
||||
onError={onError}
|
||||
onSuccess={onSaveRefusalSuccess}/>
|
||||
</FabPanel>
|
||||
@ -131,4 +133,4 @@ const SupportingDocumentsValidationWrapper: React.FC<SupportingDocumentsValidati
|
||||
|
||||
export { SupportingDocumentsValidationWrapper as SupportingDocumentsValidation };
|
||||
|
||||
Application.Components.component('supportingDocumentsValidation', react2angular(SupportingDocumentsValidationWrapper, ['operator', 'member', 'onSuccess', 'onError']));
|
||||
Application.Components.component('supportingDocumentsValidation', react2angular(SupportingDocumentsValidationWrapper, ['operator', 'member', 'onSuccess', 'onError', 'documentType']));
|
||||
|
@ -0,0 +1,97 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Member } from '../../models/member';
|
||||
import { Child } from '../../models/child';
|
||||
import { FabButton } from '../base/fab-button';
|
||||
import { CaretDown, User, Users, PencilSimple, Trash } from 'phosphor-react';
|
||||
import { ChildItem } from '../family-account/child-item';
|
||||
|
||||
interface MembersListItemProps {
|
||||
member: Member,
|
||||
onError: (message: string) => void,
|
||||
onSuccess: (message: string) => void
|
||||
onEditChild: (child: Child) => void;
|
||||
onDeleteChild: (child: Child, error: string) => void;
|
||||
onDeleteMember: (memberId: number) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Members list
|
||||
*/
|
||||
export const MembersListItem: React.FC<MembersListItemProps> = ({ member, onError, onEditChild, onDeleteChild, onDeleteMember }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
const [childrenList, setChildrenList] = useState(false);
|
||||
|
||||
/**
|
||||
* Redirect to the given user edition page
|
||||
*/
|
||||
const toMemberEdit = (memberId: number): void => {
|
||||
window.location.href = `/#!/admin/members/${memberId}/edit`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div key={member.id} className={`members-list-item ${member.validated_at ? 'is-validated' : ''} ${member.need_completion ? 'is-incomplet' : ''}`}>
|
||||
<div className="left-col">
|
||||
<div className='status'>
|
||||
{(member.children.length > 0)
|
||||
? <Users size={24} weight="bold" />
|
||||
: <User size={24} weight="bold" />
|
||||
}
|
||||
</div>
|
||||
{(member.children.length > 0) &&
|
||||
<FabButton onClick={() => setChildrenList(!childrenList)} className={`toggle ${childrenList ? 'open' : ''}`}>
|
||||
<CaretDown size={24} weight="bold" />
|
||||
</FabButton>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className="member">
|
||||
<div className="member-infos">
|
||||
<div>
|
||||
<span>{t('app.admin.members_list_item.surname')}</span>
|
||||
<p>{member.profile.last_name}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span>{t('app.admin.members_list_item.first_name')}</span>
|
||||
<p>{member.profile.first_name}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span>{t('app.admin.members_list_item.phone')}</span>
|
||||
<p>{member.profile.phone || '---'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span>{t('app.admin.members_list_item.email')}</span>
|
||||
<p>{member.email}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span>{t('app.admin.members_list_item.group')}</span>
|
||||
<p>{member.group.name}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span>{t('app.admin.members_list_item.subscription')}</span>
|
||||
<p>{member.subscribed_plan?.name || '---'}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="member-actions edit-destroy-buttons">
|
||||
<FabButton onClick={() => toMemberEdit(member.id)} className="edit-btn">
|
||||
<PencilSimple size={20} weight="fill" />
|
||||
</FabButton>
|
||||
<FabButton onClick={() => onDeleteMember(member.id)} className="delete-btn">
|
||||
<Trash size={20} weight="fill" />
|
||||
</FabButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ (member.children.length > 0) &&
|
||||
<div className={`member-children ${childrenList ? 'open' : ''}`}>
|
||||
<hr />
|
||||
{member.children.map((child: Child) => (
|
||||
<ChildItem key={child.id} child={child} size='sm' onEdit={onEditChild} onDelete={onDeleteChild} onError={onError} />
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
89
app/frontend/src/javascript/components/user/members-list.tsx
Normal file
89
app/frontend/src/javascript/components/user/members-list.tsx
Normal file
@ -0,0 +1,89 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { IApplication } from '../../models/application';
|
||||
import { Loader } from '../base/loader';
|
||||
import { react2angular } from 'react2angular';
|
||||
import { Member } from '../../models/member';
|
||||
import { MembersListItem } from './members-list-item';
|
||||
import { SupportingDocumentType } from '../../models/supporting-document-type';
|
||||
import SupportingDocumentTypeAPI from '../../api/supporting-document-type';
|
||||
import { Child } from '../../models/child';
|
||||
import { ChildModal } from '../family-account/child-modal';
|
||||
import { User } from '../../models/user';
|
||||
|
||||
declare const Application: IApplication;
|
||||
|
||||
interface MembersListProps {
|
||||
members: Member[],
|
||||
operator: User,
|
||||
onError: (message: string) => void,
|
||||
onSuccess: (message: string) => void
|
||||
onDeleteMember: (memberId: number) => void;
|
||||
onDeletedChild: (memberId: number, childId: number) => void;
|
||||
onUpdatedChild: (memberId: number, child: Child) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Members list
|
||||
*/
|
||||
export const MembersList: React.FC<MembersListProps> = ({ members, onError, onSuccess, operator, onDeleteMember, onDeletedChild, onUpdatedChild }) => {
|
||||
const [supportingDocumentsTypes, setSupportingDocumentsTypes] = useState<Array<SupportingDocumentType>>([]);
|
||||
const [child, setChild] = useState<Child>();
|
||||
const [isOpenChildModal, setIsOpenChildModal] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
SupportingDocumentTypeAPI.index({ document_type: 'Child' }).then(tData => {
|
||||
setSupportingDocumentsTypes(tData);
|
||||
});
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Open the edit child modal
|
||||
*/
|
||||
const editChild = (child: Child) => {
|
||||
setIsOpenChildModal(true);
|
||||
setChild({
|
||||
...child,
|
||||
supporting_document_files_attributes: supportingDocumentsTypes.map(t => {
|
||||
const file = child.supporting_document_files_attributes.find(f => f.supporting_document_type_id === t.id);
|
||||
return file || { supporting_document_type_id: t.id };
|
||||
})
|
||||
} as Child);
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete a child
|
||||
*/
|
||||
const handleDeleteChildSuccess = (c: Child, msg: string) => {
|
||||
onDeletedChild(c.user_id, c.id);
|
||||
onSuccess(msg);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle save child success from the API
|
||||
*/
|
||||
const handleSaveChildSuccess = (c: Child, msg: string) => {
|
||||
onUpdatedChild(c.user_id, c);
|
||||
if (msg) {
|
||||
onSuccess(msg);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="members-list">
|
||||
{members.map(member => (
|
||||
<MembersListItem key={member.id} member={member} onError={onError} onSuccess={onSuccess} onDeleteMember={onDeleteMember} onEditChild={editChild} onDeleteChild={handleDeleteChildSuccess} />
|
||||
))}
|
||||
<ChildModal child={child} isOpen={isOpenChildModal} toggleModal={() => setIsOpenChildModal(false)} onSuccess={handleSaveChildSuccess} onError={onError} supportingDocumentsTypes={supportingDocumentsTypes} operator={operator} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MembersListWrapper: React.FC<MembersListProps> = (props) => {
|
||||
return (
|
||||
<Loader>
|
||||
<MembersList {...props} />
|
||||
</Loader>
|
||||
);
|
||||
};
|
||||
|
||||
Application.Components.component('membersList', react2angular(MembersListWrapper, ['members', 'onError', 'onSuccess', 'operator', 'onDeleteMember', 'onDeletedChild', 'onUpdatedChild']));
|
@ -69,8 +69,8 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state
|
||||
snapDuration: BOOKING_SNAP,
|
||||
selectable: true,
|
||||
selectHelper: true,
|
||||
minTime: moment.duration(moment(bookingWindowStart.setting.value).format('HH:mm:ss')),
|
||||
maxTime: moment.duration(moment(bookingWindowEnd.setting.value).format('HH:mm:ss')),
|
||||
minTime: moment.duration(moment.utc(bookingWindowStart.setting.value.match(/\d{4}-\d{2}-\d{2}(?: |T)\d{2}:\d{2}:\d{2}/)[0]).format('HH:mm:ss')),
|
||||
maxTime: moment.duration(moment.utc(bookingWindowEnd.setting.value.match(/\d{4}-\d{2}-\d{2}(?: |T)\d{2}:\d{2}:\d{2}/)[0]).format('HH:mm:ss')),
|
||||
select (start, end, jsEvent, view) {
|
||||
return calendarSelectCb(start, end, jsEvent, view);
|
||||
},
|
||||
|
@ -291,7 +291,7 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce',
|
||||
Member.delete(
|
||||
{ id: memberId },
|
||||
function () {
|
||||
$scope.members.splice(findItemIdxById($scope.members, memberId), 1);
|
||||
$scope.members = _.filter($scope.members, function (m) { return m.id !== memberId; });
|
||||
return growl.success(_t('app.admin.members.member_successfully_deleted'));
|
||||
},
|
||||
function (error) {
|
||||
@ -303,6 +303,32 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce',
|
||||
);
|
||||
};
|
||||
|
||||
$scope.onDeletedChild = function (memberId, childId) {
|
||||
$scope.members = $scope.members.map(function (member) {
|
||||
if (member.id === memberId) {
|
||||
member.children = _.filter(member.children, function (c) { return c.id !== childId; });
|
||||
return member;
|
||||
}
|
||||
return member;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.onUpdatedChild = function (memberId, child) {
|
||||
$scope.members = $scope.members.map(function (member) {
|
||||
if (member.id === memberId) {
|
||||
member.children = member.children.map(function (c) {
|
||||
if (c.id === child.id) {
|
||||
return child;
|
||||
}
|
||||
return c;
|
||||
});
|
||||
console.log(member.children);
|
||||
return member;
|
||||
}
|
||||
return member;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Ask for confirmation then delete the specified administrator
|
||||
* @param admins {Array} full list of administrators
|
||||
@ -588,6 +614,20 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce',
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered in case of error
|
||||
*/
|
||||
$scope.onError = (message) => {
|
||||
growl.error(message);
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered in case of success
|
||||
*/
|
||||
$scope.onSuccess = (message) => {
|
||||
growl.success(message);
|
||||
};
|
||||
|
||||
/* PRIVATE SCOPE */
|
||||
|
||||
/**
|
||||
|
@ -71,8 +71,8 @@ Application.Controllers.controller('SettingsController', ['$scope', '$rootScope'
|
||||
$scope.subscriptionExplicationsAlert = { name: 'subscription_explications_alert', value: settingsPromise.subscription_explications_alert };
|
||||
$scope.eventExplicationsAlert = { name: 'event_explications_alert', value: settingsPromise.event_explications_alert };
|
||||
$scope.spaceExplicationsAlert = { name: 'space_explications_alert', value: settingsPromise.space_explications_alert };
|
||||
$scope.windowStart = { name: 'booking_window_start', value: settingsPromise.booking_window_start };
|
||||
$scope.windowEnd = { name: 'booking_window_end', value: settingsPromise.booking_window_end };
|
||||
$scope.windowStart = { name: 'booking_window_start', value: moment.utc(settingsPromise.booking_window_start).format('YYYY-MM-DD HH:mm:ss') };
|
||||
$scope.windowEnd = { name: 'booking_window_end', value: moment.utc(settingsPromise.booking_window_end).format('YYYY-MM-DD HH:mm:ss') };
|
||||
$scope.mainColorSetting = { name: 'main_color', value: settingsPromise.main_color };
|
||||
$scope.secondColorSetting = { name: 'secondary_color', value: settingsPromise.secondary_color };
|
||||
$scope.nameGenre = { name: 'name_genre', value: settingsPromise.name_genre };
|
||||
@ -487,8 +487,12 @@ Application.Controllers.controller('SettingsController', ['$scope', '$rootScope'
|
||||
|
||||
// we prevent the admin from setting the closing time before the opening time
|
||||
$scope.$watch('windowEnd.value', function (newValue, oldValue, scope) {
|
||||
if ($scope.windowStart && moment($scope.windowStart.value).isAfter(newValue)) {
|
||||
return $scope.windowEnd.value = oldValue;
|
||||
if (scope.windowStart) {
|
||||
const startTime = moment($scope.windowStart.value).format('HH:mm:ss');
|
||||
const endTime = moment(newValue).format('HH:mm:ss');
|
||||
if (startTime >= endTime) {
|
||||
scope.windowEnd.value = oldValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -204,8 +204,8 @@ Application.Controllers.controller('CalendarController', ['$scope', '$state', '$
|
||||
center: 'title',
|
||||
right: ''
|
||||
},
|
||||
minTime: moment.duration(moment(bookingWindowStart.setting.value).format('HH:mm:ss')),
|
||||
maxTime: moment.duration(moment(bookingWindowEnd.setting.value).format('HH:mm:ss')),
|
||||
minTime: moment.duration(moment.utc(bookingWindowStart.setting.value.match(/\d{4}-\d{2}-\d{2}(?: |T)\d{2}:\d{2}:\d{2}/)[0]).format('HH:mm:ss')),
|
||||
maxTime: moment.duration(moment.utc(bookingWindowEnd.setting.value.match(/\d{4}-\d{2}-\d{2}(?: |T)\d{2}:\d{2}:\d{2}/)[0]).format('HH:mm:ss')),
|
||||
defaultView: window.innerWidth <= 480 ? 'agendaDay' : 'agendaWeek',
|
||||
eventClick (event, jsEvent, view) {
|
||||
return calendarEventClickCb(event, jsEvent, view);
|
||||
|
23
app/frontend/src/javascript/controllers/children.js
Normal file
23
app/frontend/src/javascript/controllers/children.js
Normal file
@ -0,0 +1,23 @@
|
||||
'use strict';
|
||||
|
||||
Application.Controllers.controller('ChildrenController', ['$scope', 'memberPromise', 'growl',
|
||||
function ($scope, memberPromise, growl) {
|
||||
// Current user's profile
|
||||
$scope.user = memberPromise;
|
||||
|
||||
/**
|
||||
* Callback used to display a error message
|
||||
*/
|
||||
$scope.onError = function (message) {
|
||||
console.error(message);
|
||||
growl.error(message);
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback used to display a success message
|
||||
*/
|
||||
$scope.onSuccess = function (message) {
|
||||
growl.success(message);
|
||||
};
|
||||
}
|
||||
]);
|
@ -136,8 +136,8 @@ Application.Controllers.controller('EventsController', ['$scope', '$state', 'Eve
|
||||
}
|
||||
]);
|
||||
|
||||
Application.Controllers.controller('ShowEventController', ['$scope', '$state', '$rootScope', 'Event', '$uibModal', 'Member', 'Reservation', 'Price', 'CustomAsset', 'SlotsReservation', 'eventPromise', 'growl', '_t', 'Wallet', 'AuthService', 'helpers', 'dialogs', 'priceCategoriesPromise', 'settingsPromise', 'LocalPayment',
|
||||
function ($scope, $state,$rootScope, Event, $uibModal, Member, Reservation, Price, CustomAsset, SlotsReservation, eventPromise, growl, _t, Wallet, AuthService, helpers, dialogs, priceCategoriesPromise, settingsPromise, LocalPayment) {
|
||||
Application.Controllers.controller('ShowEventController', ['$scope', '$state', '$rootScope', 'Event', '$uibModal', 'Member', 'Reservation', 'Price', 'CustomAsset', 'SlotsReservation', 'eventPromise', 'growl', '_t', 'Wallet', 'AuthService', 'helpers', 'dialogs', 'priceCategoriesPromise', 'settingsPromise', 'LocalPayment', 'Child',
|
||||
function ($scope, $state,$rootScope, Event, $uibModal, Member, Reservation, Price, CustomAsset, SlotsReservation, eventPromise, growl, _t, Wallet, AuthService, helpers, dialogs, priceCategoriesPromise, settingsPromise, LocalPayment, Child) {
|
||||
/* PUBLIC SCOPE */
|
||||
|
||||
// reservations for the currently shown event
|
||||
@ -150,6 +150,9 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
|
||||
$scope.ctrl =
|
||||
{ member: {} };
|
||||
|
||||
// children for the member
|
||||
$scope.children = [];
|
||||
|
||||
// parameters for a new reservation
|
||||
$scope.reserve = {
|
||||
nbPlaces: {
|
||||
@ -160,7 +163,10 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
|
||||
toReserve: false,
|
||||
amountTotal: 0,
|
||||
totalNoCoupon: 0,
|
||||
totalSeats: 0
|
||||
totalSeats: 0,
|
||||
bookingUsers: {
|
||||
normal: []
|
||||
},
|
||||
};
|
||||
|
||||
// Discount coupon to apply to the basket, if any
|
||||
@ -195,6 +201,9 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
|
||||
// Global config: is the user validation required ?
|
||||
$scope.enableUserValidationRequired = settingsPromise.user_validation_required === 'true';
|
||||
|
||||
// Global config: is the child validation required ?
|
||||
$scope.enableChildValidationRequired = settingsPromise.child_validation_required === 'true';
|
||||
|
||||
// online payments (by card)
|
||||
$scope.onlinePayment = {
|
||||
showModal: false,
|
||||
@ -226,9 +235,9 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
|
||||
/**
|
||||
* Callback to call when the number of tickets to book changes in the current booking
|
||||
*/
|
||||
$scope.changeNbPlaces = function () {
|
||||
$scope.changeNbPlaces = function (priceType) {
|
||||
// compute the total remaining places
|
||||
let remain = $scope.event.nb_free_places - $scope.reserve.nbReservePlaces;
|
||||
let remain = ($scope.event.event_type === 'family' ? ($scope.children.length + 1) : $scope.event.nb_free_places) - $scope.reserve.nbReservePlaces;
|
||||
for (let ticket in $scope.reserve.tickets) {
|
||||
remain -= $scope.reserve.tickets[ticket];
|
||||
}
|
||||
@ -247,17 +256,41 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
|
||||
}
|
||||
}
|
||||
|
||||
if ($scope.event.event_type === 'nominative' || $scope.event.event_type === 'family') {
|
||||
const nbBookingUsers = $scope.reserve.bookingUsers[priceType].length;
|
||||
const nbReservePlaces = priceType === 'normal' ? $scope.reserve.nbReservePlaces : $scope.reserve.tickets[priceType];
|
||||
if (nbReservePlaces > nbBookingUsers) {
|
||||
_.times(nbReservePlaces - nbBookingUsers, () => {
|
||||
$scope.reserve.bookingUsers[priceType].push({ event_price_category_id: priceType === 'normal' ? null : priceType, bookedUsers: buildBookedUsersOptions() });
|
||||
});
|
||||
} else {
|
||||
_.times(nbBookingUsers - nbReservePlaces, () => {
|
||||
$scope.reserve.bookingUsers[priceType].pop();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// recompute the total price
|
||||
return $scope.computeEventAmount();
|
||||
};
|
||||
|
||||
$scope.changeBookedUser = function () {
|
||||
for (const key of Object.keys($scope.reserve.bookingUsers)) {
|
||||
for (const user of $scope.reserve.bookingUsers[key]) {
|
||||
user.bookedUsers = buildBookedUsersOptions(user.booked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback to reset the current reservation parameters
|
||||
* @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
|
||||
*/
|
||||
$scope.cancelReserve = function (e) {
|
||||
e.preventDefault();
|
||||
return resetEventReserve();
|
||||
resetEventReserve();
|
||||
updateNbReservePlaces();
|
||||
return;
|
||||
};
|
||||
|
||||
$scope.isUserValidatedByType = () => {
|
||||
@ -322,6 +355,9 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
|
||||
Member.get({ id: $scope.ctrl.member.id }, function (member) {
|
||||
$scope.ctrl.member = member;
|
||||
getReservations($scope.event.id, 'Event', $scope.ctrl.member.id);
|
||||
getChildren($scope.ctrl.member.id).then(() => {
|
||||
updateNbReservePlaces();
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -372,7 +408,7 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
|
||||
}
|
||||
, function (response) {
|
||||
// reservation failed
|
||||
growl.error(response && response.data && response.data.card && response.data.card[0] || 'server error');
|
||||
growl.error(response && response.data && _.keys(response.data)[0] && response.data[_.keys(response.data)[0]][0] || 'server error');
|
||||
// unset the attempting marker
|
||||
$scope.attempting = false;
|
||||
})
|
||||
@ -583,6 +619,38 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
|
||||
growl.error(message);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the reservation of current event is valid
|
||||
*/
|
||||
$scope.reservationIsValid = () => {
|
||||
if ($scope.event.event_type === 'nominative') {
|
||||
for (const key of Object.keys($scope.reserve.bookingUsers)) {
|
||||
for (const user of $scope.reserve.bookingUsers[key]) {
|
||||
if (!_.trim(user.name)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($scope.event.event_type === 'family') {
|
||||
for (const key of Object.keys($scope.reserve.bookingUsers)) {
|
||||
for (const user of $scope.reserve.bookingUsers[key]) {
|
||||
if (!user.booked) {
|
||||
return false;
|
||||
}
|
||||
if ($scope.enableChildValidationRequired && user.booked.type === 'Child' && !user.booked.validatedAt) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
$scope.isUnder18YearsAgo = (date) => {
|
||||
return moment(date).isAfter(moment().subtract(18, 'year'));
|
||||
}
|
||||
|
||||
/* PRIVATE SCOPE */
|
||||
|
||||
/**
|
||||
@ -602,6 +670,9 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
|
||||
// get the current user's reservations into $scope.reservations
|
||||
if ($scope.currentUser) {
|
||||
getReservations($scope.event.id, 'Event', $scope.currentUser.id);
|
||||
getChildren($scope.currentUser.id).then(function (children) {
|
||||
updateNbReservePlaces();
|
||||
});
|
||||
}
|
||||
|
||||
// watch when a coupon is applied to re-compute the total price
|
||||
@ -626,6 +697,74 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
|
||||
}).$promise.then(function (reservations) { $scope.reservations = reservations; });
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the children for the user
|
||||
* @param user_id {number} the user's id (current or managed)
|
||||
*/
|
||||
const getChildren = function (user_id) {
|
||||
return Child.query({
|
||||
user_id
|
||||
}).$promise.then(function (children) {
|
||||
$scope.children = children;
|
||||
return $scope.children;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the number of places reserved by the current user
|
||||
*/
|
||||
const hasBookedUser = function (userKey) {
|
||||
for (const key of Object.keys($scope.reserve.bookingUsers)) {
|
||||
for (const user of $scope.reserve.bookingUsers[key]) {
|
||||
if (user.booked && user.booked.key === userKey) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Build the list of options for the select box of the booked users
|
||||
* @param booked {object} the booked user
|
||||
*/
|
||||
const buildBookedUsersOptions = function (booked) {
|
||||
const options = [];
|
||||
const userKey = `user_${$scope.ctrl.member.id}`;
|
||||
if ((booked && booked.key === userKey) || !hasBookedUser(userKey)) {
|
||||
options.push({ key: userKey, name: $scope.ctrl.member.name, type: 'User', id: $scope.ctrl.member.id });
|
||||
}
|
||||
for (const child of $scope.children) {
|
||||
const key = `child_${child.id}`;
|
||||
if ((booked && booked.key === key) || !hasBookedUser(key)) {
|
||||
options.push({
|
||||
key,
|
||||
name: child.first_name + ' ' + child.last_name,
|
||||
id: child.id,
|
||||
type: 'Child',
|
||||
validatedAt: child.validated_at,
|
||||
birthday: child.birthday
|
||||
});
|
||||
}
|
||||
}
|
||||
return options;
|
||||
};
|
||||
|
||||
/**
|
||||
* update number of places available for each price category for the family event
|
||||
*/
|
||||
const updateNbReservePlaces = function () {
|
||||
if ($scope.event.event_type === 'family') {
|
||||
const maxPlaces = $scope.children.length + 1;
|
||||
if ($scope.event.nb_free_places > maxPlaces) {
|
||||
$scope.reserve.nbPlaces.normal = __range__(0, maxPlaces, true);
|
||||
for (let evt_px_cat of Array.from($scope.event.event_price_categories_attributes)) {
|
||||
$scope.reserve.nbPlaces[evt_px_cat.id] = __range__(0, maxPlaces, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a hash map implementing the Reservation specs
|
||||
* @param reserve {Object} Reservation parameters (places...)
|
||||
@ -638,7 +777,8 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
|
||||
reservable_type: 'Event',
|
||||
slots_reservations_attributes: [],
|
||||
nb_reserve_places: reserve.nbReservePlaces,
|
||||
tickets_attributes: []
|
||||
tickets_attributes: [],
|
||||
booking_users_attributes: []
|
||||
};
|
||||
|
||||
reservation.slots_reservations_attributes.push({
|
||||
@ -656,6 +796,19 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
|
||||
}
|
||||
}
|
||||
|
||||
if (event.event_type === 'nominative' || event.event_type === 'family') {
|
||||
for (const key of Object.keys($scope.reserve.bookingUsers)) {
|
||||
for (const user of $scope.reserve.bookingUsers[key]) {
|
||||
reservation.booking_users_attributes.push({
|
||||
event_price_category_id: user.event_price_category_id,
|
||||
name: user.booked ? user.booked.name : _.trim(user.name),
|
||||
booked_id: user.booked ? user.booked.id : undefined,
|
||||
booked_type: user.booked ? user.booked.type : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { reservation };
|
||||
};
|
||||
|
||||
@ -688,11 +841,15 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
|
||||
tickets: {},
|
||||
toReserve: false,
|
||||
amountTotal: 0,
|
||||
totalSeats: 0
|
||||
totalSeats: 0,
|
||||
bookingUsers: {
|
||||
normal: [],
|
||||
},
|
||||
};
|
||||
|
||||
for (let evt_px_cat of Array.from($scope.event.event_price_categories_attributes)) {
|
||||
$scope.reserve.nbPlaces[evt_px_cat.id] = __range__(0, $scope.event.nb_free_places, true);
|
||||
$scope.reserve.bookingUsers[evt_px_cat.id] = [];
|
||||
$scope.reserve.tickets[evt_px_cat.id] = 0;
|
||||
}
|
||||
|
||||
@ -815,6 +972,7 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
|
||||
$scope.reservations.push(reservation);
|
||||
});
|
||||
resetEventReserve();
|
||||
updateNbReservePlaces();
|
||||
$scope.reserveSuccess = true;
|
||||
$scope.coupon.applied = null;
|
||||
if ($scope.currentUser.role === 'admin') {
|
||||
|
@ -447,8 +447,8 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$tran
|
||||
|
||||
// fullCalendar (v2) configuration
|
||||
$scope.calendarConfig = CalendarConfig({
|
||||
minTime: moment.duration(moment(settingsPromise.booking_window_start).format('HH:mm:ss')),
|
||||
maxTime: moment.duration(moment(settingsPromise.booking_window_end).format('HH:mm:ss')),
|
||||
minTime: moment.duration(moment.utc(settingsPromise.booking_window_start.match(/\d{4}-\d{2}-\d{2}(?: |T)\d{2}:\d{2}:\d{2}/)[0]).format('HH:mm:ss')),
|
||||
maxTime: moment.duration(moment.utc(settingsPromise.booking_window_end.match(/\d{4}-\d{2}-\d{2}(?: |T)\d{2}:\d{2}:\d{2}/)[0]).format('HH:mm:ss')),
|
||||
eventClick (event, jsEvent, view) {
|
||||
return calendarEventClickCb(event, jsEvent, view);
|
||||
},
|
||||
|
@ -385,8 +385,8 @@ Application.Controllers.controller('ReserveSpaceController', ['$scope', '$transi
|
||||
|
||||
// fullCalendar (v2) configuration
|
||||
$scope.calendarConfig = CalendarConfig({
|
||||
minTime: moment.duration(moment(settingsPromise.booking_window_start).format('HH:mm:ss')),
|
||||
maxTime: moment.duration(moment(settingsPromise.booking_window_end).format('HH:mm:ss')),
|
||||
minTime: moment.duration(moment.utc(settingsPromise.booking_window_start.match(/\d{4}-\d{2}-\d{2}(?: |T)\d{2}:\d{2}:\d{2}/)[0]).format('HH:mm:ss')),
|
||||
maxTime: moment.duration(moment.utc(settingsPromise.booking_window_end.match(/\d{4}-\d{2}-\d{2}(?: |T)\d{2}:\d{2}:\d{2}/)[0]).format('HH:mm:ss')),
|
||||
eventClick (event, jsEvent, view) {
|
||||
return calendarEventClickCb(event, jsEvent, view);
|
||||
},
|
||||
|
@ -155,8 +155,8 @@ Application.Controllers.controller('ReserveTrainingController', ['$scope', '$tra
|
||||
|
||||
// fullCalendar (v2) configuration
|
||||
$scope.calendarConfig = CalendarConfig({
|
||||
minTime: moment.duration(moment(settingsPromise.booking_window_start).format('HH:mm:ss')),
|
||||
maxTime: moment.duration(moment(settingsPromise.booking_window_end).format('HH:mm:ss')),
|
||||
minTime: moment.duration(moment.utc(settingsPromise.booking_window_start.match(/\d{4}-\d{2}-\d{2}(?: |T)\d{2}:\d{2}:\d{2}/)[0]).format('HH:mm:ss')),
|
||||
maxTime: moment.duration(moment.utc(settingsPromise.booking_window_end.match(/\d{4}-\d{2}-\d{2}(?: |T)\d{2}:\d{2}:\d{2}/)[0]).format('HH:mm:ss')),
|
||||
eventClick (event, jsEvent, view) {
|
||||
return calendarEventClickCb(event, jsEvent, view);
|
||||
},
|
||||
|
@ -35,6 +35,9 @@ export default class ApiLib {
|
||||
if (file?.is_main) {
|
||||
data.set(`${name}[${attr}][${i}][is_main]`, file.is_main.toString());
|
||||
}
|
||||
if (file?.supporting_document_type_id) {
|
||||
data.set(`${name}[${attr}][${i}][supporting_document_type_id]`, file.supporting_document_type_id.toString());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (object[attr]?.attachment_files && object[attr]?.attachment_files[0]) {
|
||||
|
27
app/frontend/src/javascript/models/child.ts
Normal file
27
app/frontend/src/javascript/models/child.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { TDateISODate, TDateISO } from '../typings/date-iso';
|
||||
import { ApiFilter } from './api';
|
||||
|
||||
export interface ChildIndexFilter extends ApiFilter {
|
||||
user_id: number,
|
||||
}
|
||||
|
||||
export interface Child {
|
||||
id?: number,
|
||||
last_name: string,
|
||||
first_name: string,
|
||||
email?: string,
|
||||
phone?: string,
|
||||
birthday: TDateISODate,
|
||||
user_id: number,
|
||||
validated_at?: TDateISO,
|
||||
supporting_document_files_attributes?: Array<{
|
||||
id?: number,
|
||||
supportable_id?: number,
|
||||
supportable_type?: 'User' | 'Child',
|
||||
supporting_document_type_id: number,
|
||||
attachment?: File,
|
||||
attachment_name?: string,
|
||||
attachment_url?: string,
|
||||
_destroy?: boolean
|
||||
}>,
|
||||
}
|
@ -11,6 +11,7 @@ export interface EventPriceCategoryAttributes {
|
||||
}
|
||||
|
||||
export type RecurrenceOption = 'none' | 'day' | 'week' | 'month' | 'year';
|
||||
export type EventType = 'standard' | 'nominative' | 'family';
|
||||
|
||||
export interface Event {
|
||||
id?: number,
|
||||
@ -63,7 +64,8 @@ export interface Event {
|
||||
}>,
|
||||
recurrence: RecurrenceOption,
|
||||
recurrence_end_at: Date,
|
||||
advanced_accounting_attributes?: AdvancedAccounting
|
||||
advanced_accounting_attributes?: AdvancedAccounting,
|
||||
event_type: EventType,
|
||||
}
|
||||
|
||||
export interface EventDecoration {
|
||||
|
49
app/frontend/src/javascript/models/member.ts
Normal file
49
app/frontend/src/javascript/models/member.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { TDateISO } from '../typings/date-iso';
|
||||
import { Child } from './child';
|
||||
|
||||
export interface Member {
|
||||
maxMembers: number
|
||||
id: number
|
||||
username: string
|
||||
email: string
|
||||
profile: {
|
||||
first_name: string
|
||||
last_name: string
|
||||
phone: string
|
||||
}
|
||||
need_completion?: boolean
|
||||
group: {
|
||||
name: string
|
||||
}
|
||||
subscribed_plan?: Plan
|
||||
validated_at: TDateISO
|
||||
children: Child[]
|
||||
}
|
||||
|
||||
interface Plan {
|
||||
id: number
|
||||
base_name: string
|
||||
name: string
|
||||
amount: number
|
||||
interval: string
|
||||
interval_count: number
|
||||
training_credit_nb: number
|
||||
training_credits: [
|
||||
{
|
||||
training_id: number
|
||||
},
|
||||
{
|
||||
training_id: number
|
||||
}
|
||||
]
|
||||
machine_credits: [
|
||||
{
|
||||
machine_id: number
|
||||
hours: number
|
||||
},
|
||||
{
|
||||
machine_id: number
|
||||
hours: number
|
||||
}
|
||||
]
|
||||
}
|
@ -45,6 +45,13 @@ export interface Reservation {
|
||||
},
|
||||
total_booked_seats?: number,
|
||||
created_at?: TDateISO,
|
||||
booking_users_attributes?: {
|
||||
id: number,
|
||||
name: string,
|
||||
event_price_category_id: number,
|
||||
booked_id: number,
|
||||
booked_type: string,
|
||||
}
|
||||
}
|
||||
|
||||
export interface ReservationIndexFilter extends ApiFilter {
|
||||
|
@ -178,7 +178,8 @@ export const accountSettings = [
|
||||
'external_id',
|
||||
'user_change_group',
|
||||
'user_validation_required',
|
||||
'user_validation_required_list'
|
||||
'user_validation_required_list',
|
||||
'family_account'
|
||||
] as const;
|
||||
|
||||
export const analyticsSettings = [
|
||||
|
@ -1,12 +1,14 @@
|
||||
import { ApiFilter } from './api';
|
||||
|
||||
export interface SupportingDocumentFileIndexFilter extends ApiFilter {
|
||||
user_id: number,
|
||||
supportable_id: number,
|
||||
supportable_type?: 'User' | 'Child',
|
||||
}
|
||||
|
||||
export interface SupportingDocumentFile {
|
||||
id?: number,
|
||||
attachment?: string,
|
||||
user_id?: number,
|
||||
supportable_id?: number,
|
||||
supportable_type?: 'User' | 'Child',
|
||||
supporting_document_type_id: number,
|
||||
}
|
||||
|
@ -1,13 +1,15 @@
|
||||
import { ApiFilter } from './api';
|
||||
|
||||
export interface SupportingDocumentRefusalIndexFilter extends ApiFilter {
|
||||
user_id: number,
|
||||
supportable_id: number,
|
||||
supportable_type: 'User' | 'Child',
|
||||
}
|
||||
|
||||
export interface SupportingDocumentRefusal {
|
||||
id: number,
|
||||
message: string,
|
||||
user_id: number,
|
||||
supportable_id: number,
|
||||
supportable_type: 'User' | 'Child',
|
||||
operator_id: number,
|
||||
supporting_document_type_ids: Array<number>,
|
||||
}
|
||||
|
@ -2,10 +2,12 @@ import { ApiFilter } from './api';
|
||||
|
||||
export interface SupportingDocumentTypeIndexfilter extends ApiFilter {
|
||||
group_id?: number,
|
||||
document_type?: 'User' | 'Child'
|
||||
}
|
||||
|
||||
export interface SupportingDocumentType {
|
||||
id: number,
|
||||
name: string,
|
||||
group_ids: Array<number>
|
||||
group_ids: Array<number>,
|
||||
document_type: 'User' | 'Child'
|
||||
}
|
||||
|
@ -28,9 +28,9 @@ angular.module('application.router', ['ui.router'])
|
||||
logoBlackFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'logo-black-file' }).$promise; }],
|
||||
sharedTranslations: ['Translations', function (Translations) { return Translations.query(['app.shared', 'app.public.common']).$promise; }],
|
||||
modulesPromise: ['Setting', function (Setting) { return Setting.query({ names: "['machines_module', 'spaces_module', 'plans_module', 'invoicing_module', 'wallet_module', 'statistics_module', 'trainings_module', 'public_agenda_module', 'store_module']" }).$promise; }],
|
||||
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['public_registrations', 'store_hidden']" }).$promise; }]
|
||||
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['public_registrations', 'store_hidden', 'family_account']" }).$promise; }]
|
||||
},
|
||||
onEnter: ['$rootScope', 'logoFile', 'logoBlackFile', 'modulesPromise', 'CSRF', function ($rootScope, logoFile, logoBlackFile, modulesPromise, CSRF) {
|
||||
onEnter: ['$rootScope', 'logoFile', 'logoBlackFile', 'modulesPromise', 'settingsPromise', 'CSRF', function ($rootScope, logoFile, logoBlackFile, modulesPromise, settingsPromise, CSRF) {
|
||||
// Retrieve Anti-CSRF tokens from cookies
|
||||
CSRF.setMetaTags();
|
||||
// Application logo
|
||||
@ -47,6 +47,9 @@ angular.module('application.router', ['ui.router'])
|
||||
publicAgenda: (modulesPromise.public_agenda_module === 'true'),
|
||||
statistics: (modulesPromise.statistics_module === 'true')
|
||||
};
|
||||
$rootScope.settings = {
|
||||
familyAccount: (settingsPromise.family_account === 'true')
|
||||
};
|
||||
}]
|
||||
})
|
||||
.state('app.public', {
|
||||
@ -151,6 +154,15 @@ angular.module('application.router', ['ui.router'])
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('app.logged.dashboard.children', {
|
||||
url: '/children',
|
||||
views: {
|
||||
'main@': {
|
||||
templateUrl: '/dashboard/children.html',
|
||||
controller: 'ChildrenController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('app.logged.dashboard.settings', {
|
||||
url: '/settings',
|
||||
views: {
|
||||
@ -615,7 +627,7 @@ angular.module('application.router', ['ui.router'])
|
||||
resolve: {
|
||||
eventPromise: ['Event', '$transition$', function (Event, $transition$) { return Event.get({ id: $transition$.params().id }).$promise; }],
|
||||
priceCategoriesPromise: ['PriceCategory', function (PriceCategory) { return PriceCategory.query().$promise; }],
|
||||
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['booking_move_enable', 'booking_move_delay', 'booking_cancel_enable', 'booking_cancel_delay', 'event_explications_alert', 'online_payment_module', 'user_validation_required', 'user_validation_required_list']" }).$promise; }]
|
||||
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['booking_move_enable', 'booking_move_delay', 'booking_cancel_enable', 'booking_cancel_delay', 'event_explications_alert', 'online_payment_module', 'user_validation_required', 'user_validation_required_list', 'child_validation_required']" }).$promise; }]
|
||||
}
|
||||
})
|
||||
|
||||
|
11
app/frontend/src/javascript/services/child.js
Normal file
11
app/frontend/src/javascript/services/child.js
Normal file
@ -0,0 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
Application.Services.factory('Child', ['$resource', function ($resource) {
|
||||
return $resource('/api/children/:id',
|
||||
{ id: '@id' }, {
|
||||
update: {
|
||||
method: 'PUT'
|
||||
}
|
||||
}
|
||||
);
|
||||
}]);
|
@ -52,6 +52,9 @@
|
||||
@import "modules/events/event-form";
|
||||
@import "modules/events/update-recurrent-modal";
|
||||
@import "modules/events/events-settings.scss";
|
||||
@import "modules/family-account/child-form";
|
||||
@import "modules/family-account/child-item";
|
||||
@import "modules/family-account/children-dashboard";
|
||||
@import "modules/form/abstract-form-item";
|
||||
@import "modules/form/form-input";
|
||||
@import "modules/form/form-multi-file-upload";
|
||||
|
@ -1,6 +1,9 @@
|
||||
.edit-destroy-buttons {
|
||||
width: max-content;
|
||||
flex-shrink: 0;
|
||||
border-radius: var(--border-radius-sm);
|
||||
overflow: hidden;
|
||||
|
||||
button {
|
||||
@include btn;
|
||||
border-radius: 0;
|
||||
|
@ -30,6 +30,7 @@
|
||||
animation: 0.3s ease-out slideInFromTop;
|
||||
position: relative;
|
||||
top: 90px;
|
||||
max-width: 100vw;
|
||||
margin: auto;
|
||||
opacity: 1;
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
|
||||
|
@ -0,0 +1,43 @@
|
||||
.child-form {
|
||||
.grp {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@media (min-width: 640px) {flex-direction: row; }
|
||||
|
||||
.form-item:first-child { margin-right: 2.4rem; }
|
||||
}
|
||||
|
||||
hr { width: 100%; }
|
||||
.actions {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.document-list {
|
||||
margin-bottom: 1.6rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.6rem;
|
||||
|
||||
&-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.8rem;
|
||||
.type {
|
||||
@include text-sm;
|
||||
}
|
||||
.file,
|
||||
.missing {
|
||||
padding: 0.8rem 0.8rem 0.8rem 1.6rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border: 1px solid var(--gray-soft-dark);
|
||||
border-radius: var(--border-radius);
|
||||
p { margin: 0; }
|
||||
}
|
||||
.missing {
|
||||
background-color: var(--gray-soft-light);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
.child-item {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: min-content 1fr;
|
||||
align-items: flex-start;
|
||||
gap: 1.6rem 2.4rem;
|
||||
background-color: var(--gray-soft-lightest);
|
||||
&.lg {
|
||||
padding: 1.6rem;
|
||||
border: 1px solid var(--gray-soft-dark);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
&.sm {
|
||||
.actions button {
|
||||
height: 3rem !important;
|
||||
min-height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
& > div:not(.actions) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
span {
|
||||
@include text-xs;
|
||||
color: var(--gray-hard-light);
|
||||
}
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
@include text-base(600);
|
||||
}
|
||||
&.sm p {
|
||||
@include text-sm(500);
|
||||
}
|
||||
|
||||
.status {
|
||||
grid-row: 1/5;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
&.is-validated .status svg {
|
||||
color: var(--success-dark);
|
||||
}
|
||||
|
||||
.actions {
|
||||
align-self: center;
|
||||
justify-self: flex-end;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
grid-template-columns: min-content repeat(3, 1fr);
|
||||
.status { grid-row: auto; }
|
||||
.actions {
|
||||
grid-column-end: -1;
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
grid-template-columns: min-content repeat(3, 1fr) max-content;
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
.children-dashboard {
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
padding-bottom: 6rem;
|
||||
@include grid-col(12);
|
||||
gap: 3.2rem;
|
||||
align-items: flex-start;
|
||||
|
||||
header {
|
||||
@include header();
|
||||
padding-bottom: 0;
|
||||
grid-column: 2 / -2;
|
||||
}
|
||||
.children-list {
|
||||
grid-column: 2 / -2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
}
|
@ -13,6 +13,8 @@
|
||||
margin-bottom: 1.6rem;
|
||||
}
|
||||
|
||||
.placeholder { color: var(--gray-soft-darkest); }
|
||||
|
||||
.actions {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
|
@ -2,3 +2,108 @@
|
||||
width: 16px;
|
||||
height: 21px;
|
||||
}
|
||||
|
||||
.members-list {
|
||||
width: 100%;
|
||||
margin: 2.4rem 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2.4rem;
|
||||
|
||||
&-item {
|
||||
width: 100%;
|
||||
padding: 1.6rem;
|
||||
display: grid;
|
||||
grid-template-columns: 48px 1fr;
|
||||
gap: 0 2.4rem;
|
||||
border: 1px solid var(--gray-soft-dark);
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--gray-soft-lightest);
|
||||
&.is-validated .left-col .status svg { color: var(--success-dark); }
|
||||
&.is-incomplet .left-col .status svg { color: var(--alert); }
|
||||
|
||||
.left-col {
|
||||
grid-row: span 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
.status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.toggle {
|
||||
height: fit-content;
|
||||
background-color: var(--gray-soft);
|
||||
border: none;
|
||||
svg { transition: transform 0.5s ease-in-out; }
|
||||
&.open svg { transform: rotate(-180deg); }
|
||||
}
|
||||
}
|
||||
|
||||
.member {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2.4rem;
|
||||
&-infos {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
gap: 1.6rem;
|
||||
|
||||
& > div:not(.actions) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
span {
|
||||
@include text-xs;
|
||||
color: var(--gray-hard-light);
|
||||
}
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
@include text-base(600);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
}
|
||||
&-actions {
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.member-children {
|
||||
max-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.6rem;
|
||||
overflow-y: hidden;
|
||||
transition: max-height 0.5s ease-in-out;
|
||||
&.open {
|
||||
max-height: 17rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
hr { margin: 1.6rem 0 0; }
|
||||
.child-item:last-of-type { padding-bottom: 0; }
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.member-infos {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
.member {
|
||||
flex-direction: row;
|
||||
&-actions {
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (min-width: 1220px) {
|
||||
.member-infos {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -37,6 +37,7 @@
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: 1.6rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
@ -64,12 +65,28 @@
|
||||
width: 20%;
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
.buttons {
|
||||
.edit-btn {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.document-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(min-content, 50rem));
|
||||
gap: 1.6rem;
|
||||
|
||||
&-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.8rem;
|
||||
.type {
|
||||
@include text-sm;
|
||||
}
|
||||
.file {
|
||||
padding: 0.8rem 0.8rem 0.8rem 1.6rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border: 1px solid var(--gray-soft-dark);
|
||||
border-radius: var(--border-radius);
|
||||
p { margin: 0; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
.user-validation {
|
||||
.user-validation, .child-validation {
|
||||
label {
|
||||
margin-bottom: 0;
|
||||
vertical-align: middle;
|
||||
@ -9,3 +9,7 @@
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
.child-validation {
|
||||
margin: 0 0 2rem;
|
||||
text-align: center;
|
||||
}
|
@ -29,7 +29,11 @@
|
||||
<tbody>
|
||||
<tr ng-repeat="reservation in reservations" ng-class="{'disabled': isCancelled(reservation)}">
|
||||
<td class="text-c">
|
||||
<a ui-sref="app.logged.members_show({id: reservation.user_id})">{{ reservation.user_full_name }} </a>
|
||||
<a ui-sref="app.logged.members_show({id: reservation.user_id})" ng-if="event.event_type === 'standard'">{{ reservation.user_full_name }} </a>
|
||||
<div ng-repeat="bu in reservation.booking_users_attributes">
|
||||
<span ng-if="bu.booked_type !== 'User'">{{bu.name}}</span>
|
||||
<a ui-sref="app.logged.members_show({id: bu.booked_id})" ng-if="bu.booked_type === 'User'">{{bu.name}}</a>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ reservation.created_at | amDateFormat:'LL LTS' }}</td>
|
||||
<td>
|
||||
|
@ -62,10 +62,15 @@
|
||||
|
||||
</uib-tab>
|
||||
|
||||
<uib-tab heading="{{ 'app.shared.user_admin.children' | translate }}" ng-if="$root.settings.familyAccount">
|
||||
<children-dashboard user="user" operator="currentUser" admin-panel="true" on-success="onSuccess" on-error="onError" />
|
||||
</uib-tab>
|
||||
|
||||
<uib-tab heading="{{ 'app.admin.members_edit.supporting_documents' | translate }}" ng-show="hasProofOfIdentityTypes">
|
||||
<supporting-documents-validation
|
||||
operator="currentUser"
|
||||
member="user"
|
||||
document-type="User"
|
||||
on-error="onError"
|
||||
on-success="onSuccess" />
|
||||
</uib-tab>
|
||||
@ -208,10 +213,20 @@
|
||||
<span ng-if="r.nb_reserve_places > 0">
|
||||
<br/>
|
||||
<span translate translate-values="{ NUMBER: r.nb_reserve_places }">{{ 'app.admin.members_edit.NUMBER_full_price_tickets_reserved' }}</span>
|
||||
<span ng-repeat="bu in r.booking_users_attributes | filter:{event_price_category_id:null}">
|
||||
<br/>
|
||||
<span ng-if="bu.booked_type !== 'User'">{{bu.name}}</span>
|
||||
<a ui-sref="app.logged.members_show({id: bu.booked_id})" ng-if="bu.booked_type === 'User'">{{bu.name}}</a>
|
||||
</span>
|
||||
</span>
|
||||
<span ng-repeat="ticket in r.tickets">
|
||||
<br/>
|
||||
<span translate translate-values="{ NUMBER: ticket.booked, NAME: ticket.price_category.name }">{{ 'app.admin.members_edit.NUMBER_NAME_tickets_reserved' }}</span>
|
||||
<span ng-repeat="bu in r.booking_users_attributes | filter:{event_price_category_id:ticket.event_price_category_id}">
|
||||
<br/>
|
||||
<span ng-if="bu.booked_type !== 'User'">{{bu.name}}</span>
|
||||
<a ui-sref="app.logged.members_show({id: bu.booked_id})" ng-if="bu.booked_type === 'User'">{{bu.name}}</a>
|
||||
</span>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -17,11 +17,12 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12">
|
||||
<button type="button" class="btn btn-warning m-t m-b" ui-sref="app.admin.members_new" translate>
|
||||
<button type="button" class="btn btn-warning m-b" ui-sref="app.admin.members_new" translate>
|
||||
{{ 'app.admin.members.add_a_new_member' }}
|
||||
</button>
|
||||
<div class="pull-right exports-buttons" ng-show="isAuthorized('admin')">
|
||||
<div class="pull-right exports-buttons m-b" ng-show="isAuthorized('admin')">
|
||||
<a class="btn btn-default" ng-href="api/members/export_members.xlsx" target="export-frame" ng-click="alertExport('members')">
|
||||
<i class="fa fa-file-excel-o"></i> {{ 'app.admin.members.members' | translate }}
|
||||
</a>
|
||||
@ -34,46 +35,10 @@
|
||||
<iframe name="export-frame" height="0" width="0" class="none"></iframe>
|
||||
</div>
|
||||
|
||||
<table class="table members-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:4%" class="hidden-xs" ng-if="enableUserValidationRequired"></th>
|
||||
<th style="width:8%" ng-show="displayUsername"><a ng-click="setOrderMember('username')">{{ 'app.admin.members.username' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': member.order=='username', 'fa fa-sort-alpha-desc': member.order=='-username', 'fa fa-arrows-v': member.order }"></i></a></th>
|
||||
<th style="width:14%"><a ng-click="setOrderMember('last_name')">{{ 'app.admin.members.surname' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': member.order=='last_name', 'fa fa-sort-alpha-desc': member.order=='-last_name', 'fa fa-arrows-v': member.order }"></i></a></th>
|
||||
<th style="width:14%"><a ng-click="setOrderMember('first_name')">{{ 'app.admin.members.first_name' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': member.order=='first_name', 'fa fa-sort-alpha-desc': member.order=='-first_name', 'fa fa-arrows-v': member.order }"></i></a></th>
|
||||
<th style="width:14%" class="hidden-xs"><a ng-click="setOrderMember('email')">{{ 'app.admin.members.email' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': member.order=='email', 'fa fa-sort-alpha-desc': member.order=='-email', 'fa fa-arrows-v': member.order }"></i></a></th>
|
||||
<th style="width:8%" class="hidden-xs hidden-sm hidden-md"><a ng-click="setOrderMember('phone')">{{ 'app.admin.members.phone' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-numeric-asc': member.order=='phone', 'fa fa-sort-numeric-desc': member.order=='-phone', 'fa fa-arrows-v': member.order }"></i></a></th>
|
||||
<th style="width:13%" class="hidden-xs hidden-sm"><a ng-click="setOrderMember('group')">{{ 'app.admin.members.user_type' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': member.order=='group', 'fa fa-sort-alpha-desc': member.order=='-group', 'fa fa-arrows-v': member.order }"></i></a></th>
|
||||
<th style="width:13%" class="hidden-xs hidden-sm hidden-md"><a ng-click="setOrderMember('plan')">{{ 'app.admin.members.subscription' | translate }} <i class="fa fa-arrows-v" ng-class="{'fa fa-sort-alpha-asc': member.order=='plan', 'fa fa-sort-alpha-desc': member.order=='-plan', 'fa fa-arrows-v': member.order }"></i></a></th>
|
||||
<th style="width:12%" class="buttons-col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="m in members">
|
||||
<td class="text-center" ng-if="enableUserValidationRequired">
|
||||
<span ng-class="{ 'text-success': !!m.validated_at }"><i class="fa fa-user-check"></i></span>
|
||||
</td>
|
||||
<td class="text-c" ng-show="displayUsername">{{ m.username }}</td>
|
||||
<td class="text-c">{{ m.profile.last_name }}</td>
|
||||
<td class="text-c">{{ m.profile.first_name }}</td>
|
||||
<td class="hidden-xs">{{ m.email }}</td>
|
||||
<td class="hidden-xs hidden-sm hidden-md">{{ m.profile.phone }}</td>
|
||||
<td class="text-u-c text-sm hidden-xs hidden-sm">{{ m.group.name }}</td>
|
||||
<td class="hidden-xs hidden-sm hidden-md">{{ m.subscribed_plan | humanReadablePlanName }}</td>
|
||||
<td>
|
||||
<div class="buttons">
|
||||
<button class="btn btn-default edit-member" ui-sref="app.admin.members_edit({id: m.id})">
|
||||
<i class="fa fa-edit"></i>
|
||||
</button>
|
||||
<button class="btn btn-danger delete-member" ng-click="deleteMember(m.id)" ng-show="isAuthorized('admin')">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
<span class="label label-danger text-white" ng-show="m.need_completion" translate>{{ 'app.shared.user_admin.incomplete_profile' }}</span>
|
||||
<div>
|
||||
<members-list members="members" on-success="onSuccess" on-error="onError" operator="currentUser" on-delete-member="deleteMember" on-deleted-child="onDeletedChild" on-updated-child="onUpdatedChild" />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="text-center">
|
||||
<button class="btn btn-warning show-more" ng-click="showNextMembers()" ng-hide="member.noMore"><i class="fa fa-search-plus" aria-hidden="true"></i> {{ 'app.admin.members.display_more_users' | translate }}</button>
|
||||
</div>
|
||||
|
@ -51,6 +51,30 @@
|
||||
<user-validation-setting on-success="onSuccess" on-error="onError" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h3 class="m-l" translate>{{ 'app.admin.settings.family_account' }}</h3>
|
||||
<p class="alert alert-warning m-h-md" ng-bind-html="'app.admin.settings.family_account_info_html' | translate"></p>
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<boolean-setting name="'family_account'"
|
||||
settings="allSettings"
|
||||
label="'app.admin.settings.enable_family_account' | translate"
|
||||
on-success="onSuccess"
|
||||
on-error="onError">
|
||||
</div>
|
||||
<div class="col-md-10 col-md-offset-1">
|
||||
<boolean-setting name="'child_validation_required'"
|
||||
settings="allSettings"
|
||||
label="'app.admin.settings.child_validation_required_label' | translate"
|
||||
on-success="onSuccess"
|
||||
on-error="onError">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<supporting-documents-types-list on-success="onSuccess" on-error="onError" document-type="'Child'" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<h3 class="m-l" translate>{{ 'app.admin.settings.captcha' }}</h3>
|
||||
<p class="alert alert-warning m-h-md" ng-bind-html="'app.admin.settings.captcha_info_html' | translate"></p>
|
||||
@ -156,4 +180,4 @@
|
||||
</div>
|
||||
|
||||
|
||||
<supporting-documents-types-list on-success="onSuccess" on-error="onError"/>
|
||||
<supporting-documents-types-list on-success="onSuccess" on-error="onError" document-type="'User'" />
|
||||
|
11
app/frontend/templates/dashboard/children.html
Normal file
11
app/frontend/templates/dashboard/children.html
Normal file
@ -0,0 +1,11 @@
|
||||
<div>
|
||||
<section class="heading">
|
||||
<div class="row no-gutter">
|
||||
<ng-include src="'/dashboard/nav.html'"></ng-include>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
<children-dashboard user="currentUser" operator="currentUser" on-success="onSuccess" on-error="onError" />
|
||||
</div>
|
@ -26,12 +26,20 @@
|
||||
translate-values="{NUMBER: r.nb_reserve_places}">
|
||||
{{ 'app.logged.dashboard.events.NUMBER_normal_places_reserved' }}
|
||||
</span>
|
||||
<span ng-repeat="bu in r.booking_users_attributes | filter:{event_price_category_id:null}">
|
||||
<br/>
|
||||
<span>{{bu.name}}</span>
|
||||
</span>
|
||||
<span ng-repeat="ticket in r.tickets">
|
||||
<br/>
|
||||
<span translate
|
||||
translate-values="{NUMBER: ticket.booked, NAME: ticket.price_category.name}">
|
||||
{{ 'app.logged.dashboard.events.NUMBER_of_NAME_places_reserved' }}
|
||||
</span>
|
||||
<span ng-repeat="bu in r.booking_users_attributes | filter:{event_price_category_id:ticket.event_price_category_id}">
|
||||
<br/>
|
||||
<span>{{bu.name}}</span>
|
||||
</span>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -11,6 +11,7 @@
|
||||
<h4 class="m-l text-sm" translate>{{ 'app.public.common.dashboard' }}</h4>
|
||||
<ul class="nav-page nav nav-pills text-u-c text-sm">
|
||||
<li ui-sref-active="active"><a class="text-black" ui-sref="app.logged.dashboard.profile" translate>{{ 'app.public.common.my_profile' }}</a></li>
|
||||
<li ng-show="$root.settings.familyAccount" ui-sref-active="active"><a class="text-black" ui-sref="app.logged.dashboard.children" translate>{{ 'app.public.common.my_children' }}</a></li>
|
||||
<li ui-sref-active="active"><a class="text-black" ui-sref="app.logged.dashboard.settings" translate>{{ 'app.public.common.my_settings' }}</a></li>
|
||||
<li ng-if="!isAuthorized(['admin', 'manager']) && hasProofOfIdentityTypes" ui-sref-active="active"><a class="text-black" ui-sref="app.logged.dashboard.supporting_document_files" translate>{{ 'app.public.common.my_supporting_documents_files' }}</a></li>
|
||||
<li ui-sref-active="active"><a class="text-black" ui-sref="app.logged.dashboard.projects" translate>{{ 'app.public.common.my_projects' }}</a></li>
|
||||
|
@ -49,7 +49,7 @@
|
||||
|
||||
<div class="col-sm-12 col-md-12 col-lg-4">
|
||||
|
||||
<section class="widget panel b-a m" ng-if="event.event_files_attributes">
|
||||
<section class="widget panel b-a m" ng-if="event.event_files_attributes.length">
|
||||
<div class="panel-heading b-b">
|
||||
<span class="badge bg-warning pull-right">{{event.event_files_attributes.length}}</span>
|
||||
<h3 translate>{{ 'app.public.events_show.downloadable_documents' }}</h3>
|
||||
@ -72,8 +72,11 @@
|
||||
</div>
|
||||
|
||||
<div class="panel-content wrapper">
|
||||
<div>
|
||||
<span ng-if="event.event_type === 'nominative'" class="v-middle badge text-base bg-event" translate="">{{ 'app.public.events_show.event_type.nominative' }}</span>
|
||||
<span ng-if="event.event_type === 'family'" class="v-middle badge text-base bg-event" translate="">{{ 'app.public.events_show.event_type.family' }}</span>
|
||||
</div>
|
||||
|
||||
<h5>{{event.category.name}}</h5>
|
||||
<dl class="text-sm">
|
||||
<dt ng-repeat="theme in event.event_themes">
|
||||
<i class="fa fa-tags" aria-hidden="true"></i> {{theme.name}}
|
||||
@ -116,16 +119,78 @@
|
||||
<div class="row">
|
||||
<label class="col-sm-6 control-label">{{ 'app.public.events_show.full_price_' | translate }} <span class="text-blue">{{event.amount | currency}}</span></label>
|
||||
<div class="col-sm-6">
|
||||
<select ng-model="reserve.nbReservePlaces" ng-change="changeNbPlaces()" ng-options="i for i in reserve.nbPlaces.normal">
|
||||
<select ng-model="reserve.nbReservePlaces" ng-change="changeNbPlaces('normal')" ng-options="i for i in reserve.nbPlaces.normal">
|
||||
</select> {{ 'app.public.events_show.ticket' | translate:{NUMBER:reserve.nbReservePlaces} }}
|
||||
</div>
|
||||
<div class="col-sm-12 m-b" ng-if="event.event_type === 'nominative' && reserve.nbReservePlaces > 0">
|
||||
<div ng-repeat="user in reserve.bookingUsers.normal">
|
||||
<label class="" translate>{{ 'app.public.events_show.last_name_and_first_name '}}</label>
|
||||
<input type="text" class="form-control" ng-model="user.name" ng-required="true">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 m-b" ng-if="ctrl.member.id && event.event_type === 'family' && reserve.nbReservePlaces > 0">
|
||||
<div ng-repeat="user in reserve.bookingUsers.normal">
|
||||
<label class="" translate>{{ 'app.public.events_show.last_name_and_first_name '}}</label>
|
||||
<select ng-model="user.booked"
|
||||
ng-options="option.name for option in user.bookedUsers track by option.key"
|
||||
ng-change="changeBookedUser()"
|
||||
name="booked"
|
||||
ng-required="true"
|
||||
class="form-control">
|
||||
<option value=""></option>
|
||||
</select>
|
||||
<uib-alert type="danger" ng-if="enableChildValidationRequired && user.booked && user.booked.type === 'Child' && !user.booked.validatedAt" style="margin-bottom: 0.8rem;">
|
||||
<span class="text-sm">
|
||||
<i class="fa fa-warning"></i>
|
||||
<span translate>{{ 'app.shared.cart.child_validation_required_alert' }}</span>
|
||||
</span>
|
||||
</uib-alert>
|
||||
<uib-alert type="danger" ng-if="user.booked && user.booked.type === 'Child' && !isUnder18YearsAgo(user.booked.birthday)" style="margin-bottom: 0.8rem;">
|
||||
<span class="text-sm">
|
||||
<i class="fa fa-warning"></i>
|
||||
<span translate>{{ 'app.shared.cart.child_birthday_must_be_under_18_years_ago_alert' }}</span>
|
||||
</span>
|
||||
</uib-alert>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" ng-repeat="price in event.event_price_categories_attributes">
|
||||
<label class="col-sm-6 control-label">{{price.category.name}} : <span class="text-blue">{{price.amount | currency}}</span></label>
|
||||
<div class="col-sm-6">
|
||||
<select ng-model="reserve.tickets[price.id]" ng-change="changeNbPlaces()" ng-options="i for i in reserve.nbPlaces[price.id]">
|
||||
<select ng-model="reserve.tickets[price.id]" ng-change="changeNbPlaces(price.id)" ng-options="i for i in reserve.nbPlaces[price.id]">
|
||||
</select> {{ 'app.public.events_show.ticket' | translate:{NUMBER:reserve.tickets[price.id]} }}
|
||||
</div>
|
||||
<div class="col-sm-12 m-b" ng-if="event.event_type === 'nominative' && reserve.tickets[price.id] > 0">
|
||||
<div ng-repeat="user in reserve.bookingUsers[price.id]">
|
||||
<label class="" translate>{{ 'app.public.events_show.last_name_and_first_name '}}</label>
|
||||
<input type="text" class="form-control" ng-model="user.name" ng-required="true">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 m-b" ng-if="ctrl.member.id && event.event_type === 'family' && reserve.tickets[price.id] > 0">
|
||||
<div ng-repeat="user in reserve.bookingUsers[price.id]">
|
||||
<label class="" translate>{{ 'app.public.events_show.last_name_and_first_name '}}</label>
|
||||
<select ng-model="user.booked"
|
||||
ng-options="option.name for option in user.bookedUsers track by option.key"
|
||||
ng-change="changeBookedUser()"
|
||||
name="booked"
|
||||
ng-required="true"
|
||||
class="form-control">
|
||||
<option value=""></option>
|
||||
</select>
|
||||
<uib-alert type="danger" ng-if="enableChildValidationRequired && user.booked && user.booked.type === 'Child' && !user.booked.validatedAt">
|
||||
<p class="text-sm">
|
||||
<i class="fa fa-warning"></i>
|
||||
<span translate>{{ 'app.shared.cart.child_validation_required_alert' }}</span>
|
||||
</p>
|
||||
</uib-alert>
|
||||
<uib-alert type="danger" ng-if="user.booked && user.booked.type === 'Child' && !isUnder18YearsAgo(user.booked.birthday)">
|
||||
<p class="text-sm">
|
||||
<i class="fa fa-warning"></i>
|
||||
<span translate>{{ 'app.shared.cart.child_birthday_must_be_under_18_years_ago_alert' }}</span>
|
||||
</p>
|
||||
</uib-alert>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="currentUser.role == 'admin'" class="m-t">
|
||||
@ -157,7 +222,7 @@
|
||||
<div ng-hide="isCancelled(reservation)" class="well well-warning">
|
||||
<div class="font-sbold text-u-c text-sm">{{ 'app.public.events_show.you_booked_DATE' | translate:{DATE:(reservation.created_at | amDateFormat:'L LT')} }}</div>
|
||||
<div class="font-sbold text-sm" ng-if="reservation.nb_reserve_places > 0">{{ 'app.public.events_show.full_price_' | translate }} {{reservation.nb_reserve_places}} {{ 'app.public.events_show.ticket' | translate:{NUMBER:reservation.nb_reserve_places} }}</div>
|
||||
<div class="font-sbold text-sm" ng-repeat="ticket in reservation.tickets">
|
||||
<div class="font-sbold text-sm" ng-repeat="ticket in reservation.tickets_attributes">
|
||||
{{ticket.event_price_category.price_category.name}} : {{ticket.booked}} {{ 'app.public.events_show.ticket' | translate:{NUMBER:ticket.booked} }}
|
||||
</div>
|
||||
<div class="clear" ng-if="event.recurrence_events.length > 0 && reservationCanModify(reservation)">
|
||||
@ -190,11 +255,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-footer no-padder ng-scope" ng-if="event.amount">
|
||||
<div class="panel-footer no-padder ng-scope" ng-if="event.amount && reservationIsValid()">
|
||||
<button class="btn btn-valid btn-info btn-block p-l btn-lg text-u-c r-b text-base" ng-click="payEvent()" ng-if="reserve.totalSeats > 0">{{ 'app.public.events_show.confirm_and_pay' | translate }} {{reserve.amountTotal | currency}}</button>
|
||||
</div>
|
||||
|
||||
<div class="panel-footer no-padder ng-scope" ng-if="event.amount == 0">
|
||||
<div class="panel-footer no-padder ng-scope" ng-if="event.amount == 0 && reservationIsValid()">
|
||||
<button class="btn btn-valid btn-info btn-block p-l btn-lg text-u-c r-b text-base" ng-click="validReserveEvent()" ng-if="reserve.totalSeats > 0" ng-disabled="attempting">{{ 'app.shared.buttons.confirm' | translate }}</button>
|
||||
</div>
|
||||
|
||||
|
@ -12,8 +12,11 @@
|
||||
</ui-select-choices>
|
||||
</ui-select>
|
||||
{{member}}
|
||||
<div class="alert alert-danger m-t" style="margin-bottom: 0 !important;" ng-if="enableUserValidationRequired && ctrl.member.id && !ctrl.member.validated_at">
|
||||
<uib-alert type="danger" ng-if="enableUserValidationRequired && ctrl.member.id && !ctrl.member.validated_at" style="margin-bottom: 0;">
|
||||
<span class="text-sm">
|
||||
<i class="fa fa-warning"></i>
|
||||
<span translate>{{ 'app.shared.member_select.member_not_validated' }}</span>
|
||||
</div>
|
||||
</span>
|
||||
</uib-alert>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -40,6 +40,7 @@
|
||||
</a>
|
||||
<ul uib-dropdown-menu class="animated fadeInRight">
|
||||
<li><a ui-sref="app.logged.dashboard.profile" translate>{{ 'app.public.common.my_profile' }}</a></li>
|
||||
<li ng-if="$root.settings.familyAccount"><a ui-sref="app.logged.dashboard.children" translate>{{ 'app.public.common.my_children' }}</a></li>
|
||||
<li><a ui-sref="app.logged.dashboard.settings" translate>{{ 'app.public.common.my_settings' }}</a></li>
|
||||
<li ng-if="!isAuthorized(['admin', 'manager']) && hasProofOfIdentityTypes"><a ui-sref="app.logged.dashboard.supporting_document_files" translate>{{ 'app.public.common.my_supporting_documents_files' }}</a></li>
|
||||
<li><a ui-sref="app.logged.dashboard.projects" translate>{{ 'app.public.common.my_projects' }}</a></li>
|
||||
|
@ -41,7 +41,7 @@ module ExcelHelper
|
||||
unless type.simple
|
||||
data.push hit['_source']['stat']
|
||||
styles.push nil
|
||||
types.push :string
|
||||
types.push :float
|
||||
end
|
||||
|
||||
[data, styles, types]
|
||||
|
@ -167,6 +167,8 @@ module SettingsHelper
|
||||
user_validation_required
|
||||
user_validation_required_list
|
||||
show_username_in_admin_list
|
||||
family_account
|
||||
child_validation_required
|
||||
store_module
|
||||
store_withdrawal_instructions
|
||||
store_hidden
|
||||
|
9
app/models/booking_user.rb
Normal file
9
app/models/booking_user.rb
Normal file
@ -0,0 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# BookingUser is a class for save the booking info of reservation
|
||||
# booked can be a User or a Child (polymorphic)
|
||||
class BookingUser < ApplicationRecord
|
||||
belongs_to :reservation
|
||||
belongs_to :booked, polymorphic: true
|
||||
belongs_to :event_price_category
|
||||
end
|
5
app/models/cart_item.rb
Normal file
5
app/models/cart_item.rb
Normal file
@ -0,0 +1,5 @@
|
||||
module CartItem
|
||||
def self.table_name_prefix
|
||||
"cart_item_"
|
||||
end
|
||||
end
|
@ -13,6 +13,11 @@ class CartItem::EventReservation < CartItem::Reservation
|
||||
foreign_type: 'cart_item_type', as: :cart_item
|
||||
accepts_nested_attributes_for :cart_item_reservation_slots
|
||||
|
||||
has_many :cart_item_event_reservation_booking_users, class_name: 'CartItem::EventReservationBookingUser', dependent: :destroy,
|
||||
inverse_of: :cart_item_event_reservation,
|
||||
foreign_key: 'cart_item_event_reservation_id'
|
||||
accepts_nested_attributes_for :cart_item_event_reservation_booking_users
|
||||
|
||||
belongs_to :operator_profile, class_name: 'InvoicingProfile'
|
||||
belongs_to :customer_profile, class_name: 'InvoicingProfile'
|
||||
|
||||
@ -63,6 +68,14 @@ class CartItem::EventReservation < CartItem::Reservation
|
||||
booked: t.booked
|
||||
}
|
||||
end,
|
||||
booking_users_attributes: cart_item_event_reservation_booking_users.map do |b|
|
||||
{
|
||||
event_price_category_id: b.event_price_category_id,
|
||||
booked_type: b.booked_type,
|
||||
booked_id: b.booked_id,
|
||||
name: b.name
|
||||
}
|
||||
end,
|
||||
nb_reserve_places: normal_tickets,
|
||||
statistic_profile_id: StatisticProfile.find_by(user: customer).id
|
||||
)
|
||||
|
10
app/models/cart_item/event_reservation_booking_user.rb
Normal file
10
app/models/cart_item/event_reservation_booking_user.rb
Normal file
@ -0,0 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# A relation table between a pending event reservation and reservation users for this event
|
||||
class CartItem::EventReservationBookingUser < ApplicationRecord
|
||||
self.table_name = 'cart_item_event_reservation_booking_users'
|
||||
|
||||
belongs_to :cart_item_event_reservation, class_name: 'CartItem::EventReservation', inverse_of: :cart_item_event_reservation_booking_users
|
||||
belongs_to :event_price_category, inverse_of: :cart_item_event_reservation_tickets
|
||||
belongs_to :booked, polymorphic: true
|
||||
end
|
24
app/models/child.rb
Normal file
24
app/models/child.rb
Normal file
@ -0,0 +1,24 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Child is a modal for a child of a user
|
||||
class Child < ApplicationRecord
|
||||
belongs_to :user
|
||||
|
||||
has_many :supporting_document_files, as: :supportable, dependent: :destroy
|
||||
accepts_nested_attributes_for :supporting_document_files, allow_destroy: true, reject_if: :all_blank
|
||||
has_many :supporting_document_refusals, as: :supportable, dependent: :destroy
|
||||
|
||||
validates :first_name, presence: true
|
||||
validates :last_name, presence: true
|
||||
# validates :email, presence: true, format: { with: Devise.email_regexp }
|
||||
validate :validate_age
|
||||
|
||||
# birthday should less than 18 years ago
|
||||
def validate_age
|
||||
errors.add(:birthday, I18n.t('.errors.messages.birthday_less_than_18_years_ago')) if birthday.blank? || birthday < 18.years.ago
|
||||
end
|
||||
|
||||
def full_name
|
||||
"#{(first_name || '').humanize.titleize} #{(last_name || '').humanize.titleize}"
|
||||
end
|
||||
end
|
@ -8,7 +8,7 @@ module StatConcern
|
||||
attribute :type, String
|
||||
attribute :subType, String
|
||||
attribute :date, String
|
||||
attribute :stat, Integer
|
||||
attribute :stat, Float
|
||||
attribute :userId, Integer
|
||||
attribute :gender, String
|
||||
attribute :age, Integer
|
||||
|
@ -33,6 +33,8 @@ class Event < ApplicationRecord
|
||||
|
||||
has_many :cart_item_event_reservations, class_name: 'CartItem::EventReservation', dependent: :destroy
|
||||
|
||||
validates :event_type, inclusion: { in: %w[standard nominative family] }, presence: true
|
||||
|
||||
attr_accessor :recurrence, :recurrence_end_at
|
||||
|
||||
before_save :update_nb_free_places
|
||||
|
@ -40,6 +40,8 @@ class Project < ApplicationRecord
|
||||
has_many :project_steps, dependent: :destroy
|
||||
accepts_nested_attributes_for :project_steps, allow_destroy: true
|
||||
|
||||
has_many :abuses, as: :signaled, dependent: :destroy, class_name: 'Abuse'
|
||||
|
||||
# validations
|
||||
validates :author, :name, presence: true
|
||||
|
||||
|
@ -23,6 +23,9 @@ class Reservation < ApplicationRecord
|
||||
|
||||
has_many :prepaid_pack_reservations, dependent: :destroy
|
||||
|
||||
has_many :booking_users, dependent: :destroy
|
||||
accepts_nested_attributes_for :booking_users, allow_destroy: true
|
||||
|
||||
validates :reservable_id, :reservable_type, presence: true
|
||||
validate :machine_not_already_reserved, if: -> { reservable.is_a?(Machine) }
|
||||
validate :training_not_fully_reserved, if: -> { reservable.is_a?(Training) }
|
||||
|
@ -6,7 +6,7 @@ class SupportingDocumentFile < ApplicationRecord
|
||||
mount_uploader :attachment, SupportingDocumentFileUploader
|
||||
|
||||
belongs_to :supporting_document_type
|
||||
belongs_to :user
|
||||
belongs_to :supportable, polymorphic: true
|
||||
|
||||
validates :attachment, file_size: { maximum: ENV.fetch('MAX_SUPPORTING_DOCUMENT_FILE_SIZE', 5.megabytes).to_i }
|
||||
end
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
# An admin can mark an uploaded document as refused, this will notify the member
|
||||
class SupportingDocumentRefusal < ApplicationRecord
|
||||
belongs_to :user
|
||||
belongs_to :supportable, polymorphic: true
|
||||
belongs_to :operator, class_name: 'User', inverse_of: :supporting_document_refusals
|
||||
has_many :supporting_document_refusals_types, dependent: :destroy
|
||||
has_many :supporting_document_types, through: :supporting_document_refusals_types
|
||||
|
@ -8,4 +8,6 @@ class SupportingDocumentType < ApplicationRecord
|
||||
|
||||
has_many :supporting_document_refusals_types, dependent: :destroy
|
||||
has_many :supporting_document_refusals, through: :supporting_document_refusals_types
|
||||
|
||||
validates :document_type, presence: true, inclusion: { in: %w[User Child] }
|
||||
end
|
||||
|
@ -47,12 +47,15 @@ class User < ApplicationRecord
|
||||
|
||||
has_many :accounting_periods, foreign_key: 'closed_by', dependent: :nullify, inverse_of: :user
|
||||
|
||||
has_many :supporting_document_files, dependent: :destroy
|
||||
has_many :supporting_document_refusals, dependent: :destroy
|
||||
has_many :supporting_document_files, as: :supportable, dependent: :destroy
|
||||
has_many :supporting_document_refusals, as: :supportable, dependent: :destroy
|
||||
|
||||
has_many :notifications, as: :receiver, dependent: :destroy
|
||||
has_many :notification_preferences, dependent: :destroy
|
||||
|
||||
has_many :children, dependent: :destroy
|
||||
accepts_nested_attributes_for :children, allow_destroy: true
|
||||
|
||||
# fix for create admin user
|
||||
before_save do
|
||||
email&.downcase!
|
||||
|
28
app/policies/child_policy.rb
Normal file
28
app/policies/child_policy.rb
Normal file
@ -0,0 +1,28 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Check the access policies for API::ChildrenController
|
||||
class ChildPolicy < ApplicationPolicy
|
||||
def index?
|
||||
!user.organization?
|
||||
end
|
||||
|
||||
def create?
|
||||
!user.organization? && user.id == record.user_id
|
||||
end
|
||||
|
||||
def show?
|
||||
user.privileged? || user.id == record.user_id
|
||||
end
|
||||
|
||||
def update?
|
||||
user.privileged? || user.id == record.user_id
|
||||
end
|
||||
|
||||
def destroy?
|
||||
user.privileged? || user.id == record.user_id
|
||||
end
|
||||
|
||||
def validate?
|
||||
user.privileged?
|
||||
end
|
||||
end
|
@ -46,7 +46,7 @@ class SettingPolicy < ApplicationPolicy
|
||||
external_id machines_banner_active machines_banner_text machines_banner_cta_active machines_banner_cta_label
|
||||
machines_banner_cta_url trainings_banner_active trainings_banner_text trainings_banner_cta_active trainings_banner_cta_label
|
||||
trainings_banner_cta_url events_banner_active events_banner_text events_banner_cta_active events_banner_cta_label
|
||||
events_banner_cta_url]
|
||||
events_banner_cta_url family_account child_validation_required]
|
||||
end
|
||||
|
||||
##
|
||||
|
@ -6,15 +6,11 @@ class SupportingDocumentFilePolicy < ApplicationPolicy
|
||||
user.privileged?
|
||||
end
|
||||
|
||||
def create?
|
||||
user.privileged? or record.user_id == user.id
|
||||
%w[create update download].each do |action|
|
||||
define_method "#{action}?" do
|
||||
user.privileged? ||
|
||||
(record.supportable_type == 'User' && record.supportable_id.to_i == user.id) ||
|
||||
(record.supportable_type == 'Child' && user.children.exists?(id: record.supportable_id.to_i))
|
||||
end
|
||||
|
||||
def update?
|
||||
user.privileged? or record.user_id == user.id
|
||||
end
|
||||
|
||||
def download?
|
||||
user.privileged? or record.user_id == user.id
|
||||
end
|
||||
end
|
||||
|
@ -171,7 +171,8 @@ class CartService
|
||||
event: reservable,
|
||||
cart_item_reservation_slots_attributes: cart_item[:slots_reservations_attributes],
|
||||
normal_tickets: cart_item[:nb_reserve_places],
|
||||
cart_item_event_reservation_tickets_attributes: cart_item[:tickets_attributes] || {})
|
||||
cart_item_event_reservation_tickets_attributes: cart_item[:tickets_attributes] || {},
|
||||
cart_item_event_reservation_booking_users_attributes: cart_item[:booking_users_attributes] || {})
|
||||
when Space
|
||||
CartItem::SpaceReservation.new(customer_profile: @customer.invoicing_profile,
|
||||
operator_profile: @operator.invoicing_profile,
|
||||
|
59
app/services/child_service.rb
Normal file
59
app/services/child_service.rb
Normal file
@ -0,0 +1,59 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# ChildService
|
||||
class ChildService
|
||||
def self.create(child)
|
||||
if child.save
|
||||
NotificationCenter.call type: 'notify_admin_child_created',
|
||||
receiver: User.admins_and_managers,
|
||||
attached_object: child
|
||||
all_files_are_upload = true
|
||||
SupportingDocumentType.where(document_type: 'Child').each do |sdt|
|
||||
file = sdt.supporting_document_files.find_by(supportable: child)
|
||||
all_files_are_upload = false if file.nil? || file.attachment_identifier.nil?
|
||||
end
|
||||
if all_files_are_upload
|
||||
NotificationCenter.call type: 'notify_admin_user_child_supporting_document_files_created',
|
||||
receiver: User.admins_and_managers,
|
||||
attached_object: child
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
def self.update(child, child_params)
|
||||
if child.update(child_params)
|
||||
all_files_are_upload = true
|
||||
SupportingDocumentType.where(document_type: 'Child').each do |sdt|
|
||||
file = sdt.supporting_document_files.find_by(supportable: child)
|
||||
all_files_are_upload = false if file.nil? || file.attachment_identifier.nil?
|
||||
end
|
||||
if all_files_are_upload
|
||||
NotificationCenter.call type: 'notify_admin_user_child_supporting_document_files_updated',
|
||||
receiver: User.admins_and_managers,
|
||||
attached_object: child
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
def self.validate(child, is_valid)
|
||||
is_updated = child.update(validated_at: is_valid ? Time.current : nil)
|
||||
if is_updated
|
||||
if is_valid
|
||||
NotificationCenter.call type: 'notify_user_child_is_validated',
|
||||
receiver: child.user,
|
||||
attached_object: child
|
||||
else
|
||||
NotificationCenter.call type: 'notify_user_child_is_invalidated',
|
||||
receiver: child.user,
|
||||
attached_object: child
|
||||
end
|
||||
end
|
||||
is_updated
|
||||
end
|
||||
end
|
@ -33,8 +33,8 @@ class EventService
|
||||
end
|
||||
|
||||
def date_range(starting, ending, all_day)
|
||||
start_date = Time.zone.parse(starting[:date])
|
||||
end_date = Time.zone.parse(ending[:date])
|
||||
start_date = Date.parse(starting[:date])
|
||||
end_date = Date.parse(ending[:date])
|
||||
start_time = starting[:time] ? Time.zone.parse(starting[:time]) : nil
|
||||
end_time = ending[:time] ? Time.zone.parse(ending[:time]) : nil
|
||||
if all_day || start_time.nil? || end_time.nil?
|
||||
|
@ -4,7 +4,7 @@
|
||||
class Members::ListService
|
||||
class << self
|
||||
def list(params)
|
||||
@query = User.includes(:profile, :group, :statistic_profile)
|
||||
@query = User.includes(:profile, :group, :statistic_profile, :children)
|
||||
.joins(:profile,
|
||||
:statistic_profile,
|
||||
:group,
|
||||
@ -27,10 +27,10 @@ class Members::ListService
|
||||
# ILIKE => PostgreSQL case-insensitive LIKE
|
||||
if params[:search].size.positive?
|
||||
@query = @query.where('users.username ILIKE :search OR ' \
|
||||
'profiles.first_name ILIKE :search OR ' \
|
||||
'profiles.last_name ILIKE :search OR ' \
|
||||
"profiles.first_name || ' ' || profiles.last_name ILIKE :search OR " \
|
||||
'profiles.phone ILIKE :search OR ' \
|
||||
'email ILIKE :search OR ' \
|
||||
'users.email ILIKE :search OR ' \
|
||||
"children.first_name || ' ' || children.last_name ILIKE :search OR " \
|
||||
'groups.name ILIKE :search OR ' \
|
||||
'plans.base_name ILIKE :search', search: "%#{params[:search]}%")
|
||||
end
|
||||
|
@ -39,11 +39,11 @@ module Statistics::Concerns::HelpersConcern
|
||||
|
||||
def difference_in_hours(start_at, end_at)
|
||||
if start_at.to_date == end_at.to_date
|
||||
((end_at - start_at) / 3600.0).to_i
|
||||
((end_at - start_at) / 3600.0).to_f
|
||||
else
|
||||
end_at_to_start_date = end_at.change(year: start_at.year, month: start_at.month, day: start_at.day)
|
||||
hours = ((end_at_to_start_date - start_at) / 60 / 60).to_i
|
||||
hours = ((end_at.to_date - start_at.to_date).to_i + 1) * hours if end_at.to_date > start_at.to_date
|
||||
hours = ((end_at_to_start_date - start_at) / 60 / 60).to_f
|
||||
hours = ((end_at.to_date - start_at.to_date).to_f + 1) * hours if end_at.to_date > start_at.to_date
|
||||
hours
|
||||
end
|
||||
end
|
||||
|
@ -57,7 +57,7 @@ class Statistics::FetcherService
|
||||
machine_type: r.reservable.friendly_id,
|
||||
machine_name: r.reservable.name,
|
||||
slot_dates: r.slots.map(&:start_at).map(&:to_date),
|
||||
nb_hours: (r.slots.map(&:duration).map(&:to_i).reduce(:+) / 3600.0).to_i,
|
||||
nb_hours: (r.slots.map(&:duration).map(&:to_i).reduce(:+) / 3600.0).to_f,
|
||||
ca: calcul_ca(r.original_invoice) }.merge(user_info(profile))
|
||||
yield result
|
||||
end
|
||||
@ -81,7 +81,7 @@ class Statistics::FetcherService
|
||||
space_name: r.reservable.name,
|
||||
space_type: r.reservable.slug,
|
||||
slot_dates: r.slots.map(&:start_at).map(&:to_date),
|
||||
nb_hours: (r.slots.map(&:duration).map(&:to_i).reduce(:+) / 3600.0).to_i,
|
||||
nb_hours: (r.slots.map(&:duration).map(&:to_i).reduce(:+) / 3600.0).to_f,
|
||||
ca: calcul_ca(r.original_invoice) }.merge(user_info(profile))
|
||||
yield result
|
||||
end
|
||||
|
@ -4,23 +4,32 @@
|
||||
class SupportingDocumentFileService
|
||||
def self.list(operator, filters = {})
|
||||
files = []
|
||||
if filters[:user_id].present? && (operator.privileged? || filters[:user_id].to_i == operator.id)
|
||||
files = SupportingDocumentFile.where(user_id: filters[:user_id])
|
||||
if filters[:supportable_id].present? && can_list?(operator, filters[:supportable_id], filters[:supportable_type])
|
||||
files = SupportingDocumentFile.where(supportable_id: filters[:supportable_id], supportable_type: filters[:supportable_type])
|
||||
end
|
||||
files
|
||||
end
|
||||
|
||||
def self.can_list?(operator, supportable_id, supportable_type)
|
||||
operator.privileged? ||
|
||||
(supportable_type == 'User' && supportable_id.to_i == operator.id) ||
|
||||
(supportable_type == 'Child' && operator.children.exists?(id: supportable_id.to_i))
|
||||
end
|
||||
|
||||
def self.create(supporting_document_file)
|
||||
saved = supporting_document_file.save
|
||||
|
||||
if saved
|
||||
user = User.find(supporting_document_file.user_id)
|
||||
all_files_are_upload = true
|
||||
if supporting_document_file.supportable_type == 'User'
|
||||
user = supporting_document_file.supportable
|
||||
user.group.supporting_document_types.each do |type|
|
||||
file = type.supporting_document_files.find_by(user_id: supporting_document_file.user_id)
|
||||
file = type.supporting_document_files.find_by(supportable_id: supporting_document_file.supportable_id,
|
||||
supportable_type: supporting_document_file.supportable_type)
|
||||
all_files_are_upload = false unless file
|
||||
end
|
||||
if all_files_are_upload
|
||||
end
|
||||
if all_files_are_upload && (supporting_document_file.supportable_type == 'User')
|
||||
NotificationCenter.call type: 'notify_admin_user_supporting_document_files_created',
|
||||
receiver: User.admins_and_managers,
|
||||
attached_object: user
|
||||
@ -32,13 +41,16 @@ class SupportingDocumentFileService
|
||||
def self.update(supporting_document_file, params)
|
||||
updated = supporting_document_file.update(params)
|
||||
if updated
|
||||
user = supporting_document_file.user
|
||||
all_files_are_upload = true
|
||||
if supporting_document_file.supportable_type == 'User'
|
||||
user = supporting_document_file.supportable
|
||||
user.group.supporting_document_types.each do |type|
|
||||
file = type.supporting_document_files.find_by(user_id: supporting_document_file.user_id)
|
||||
file = type.supporting_document_files.find_by(supportable_id: supporting_document_file.supportable_id,
|
||||
supportable_type: supporting_document_file.supportable_type)
|
||||
all_files_are_upload = false unless file
|
||||
end
|
||||
if all_files_are_upload
|
||||
end
|
||||
if all_files_are_upload && (supporting_document_file.supportable_type == 'User')
|
||||
NotificationCenter.call type: 'notify_admin_user_supporting_document_files_updated',
|
||||
receiver: User.admins_and_managers,
|
||||
attached_object: supporting_document_file
|
||||
|
@ -4,7 +4,10 @@
|
||||
class SupportingDocumentRefusalService
|
||||
def self.list(filters = {})
|
||||
refusals = []
|
||||
refusals = SupportingDocumentRefusal.where(user_id: filters[:user_id]) if filters[:user_id].present?
|
||||
if filters[:supportable_id].present?
|
||||
refusals = SupportingDocumentRefusal.where(supportable_id: filters[:supportable_id],
|
||||
supportable_type: filters[:supportable_type])
|
||||
end
|
||||
refusals
|
||||
end
|
||||
|
||||
@ -12,12 +15,22 @@ class SupportingDocumentRefusalService
|
||||
saved = supporting_document_refusal.save
|
||||
|
||||
if saved
|
||||
case supporting_document_refusal.supportable_type
|
||||
when 'User'
|
||||
NotificationCenter.call type: 'notify_admin_user_supporting_document_refusal',
|
||||
receiver: User.admins_and_managers,
|
||||
attached_object: supporting_document_refusal
|
||||
NotificationCenter.call type: 'notify_user_supporting_document_refusal',
|
||||
receiver: supporting_document_refusal.user,
|
||||
receiver: supporting_document_refusal.supportable,
|
||||
attached_object: supporting_document_refusal
|
||||
when 'Child'
|
||||
NotificationCenter.call type: 'notify_admin_user_child_supporting_document_refusal',
|
||||
receiver: User.admins_and_managers,
|
||||
attached_object: SupportingDocumentRefusal.last
|
||||
NotificationCenter.call type: 'notify_user_child_supporting_document_refusal',
|
||||
receiver: SupportingDocumentRefusal.last.supportable.user,
|
||||
attached_object: SupportingDocumentRefusal.last
|
||||
end
|
||||
end
|
||||
saved
|
||||
end
|
||||
|
@ -9,7 +9,7 @@ class SupportingDocumentTypeService
|
||||
|
||||
group.supporting_document_types.includes(:groups)
|
||||
else
|
||||
SupportingDocumentType.all
|
||||
SupportingDocumentType.where(document_type: filters[:document_type] || 'User')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
12
app/views/api/children/_child.json.jbuilder
Normal file
12
app/views/api/children/_child.json.jbuilder
Normal file
@ -0,0 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.extract! child, :id, :first_name, :last_name, :email, :birthday, :phone, :user_id, :validated_at
|
||||
json.supporting_document_files_attributes child.supporting_document_files do |f|
|
||||
json.id f.id
|
||||
json.supportable_id f.supportable_id
|
||||
json.supportable_type f.supportable_type
|
||||
json.supporting_document_type_id f.supporting_document_type_id
|
||||
json.attachment f.attachment.file&.filename
|
||||
json.attachment_name f.attachment_identifier
|
||||
json.attachment_url f.attachment_identifier ? "/api/supporting_document_files/#{f.id}/download" : nil
|
||||
end
|
3
app/views/api/children/create.json.jbuilder
Normal file
3
app/views/api/children/create.json.jbuilder
Normal file
@ -0,0 +1,3 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.partial! 'child', child: @child
|
5
app/views/api/children/index.json.jbuilder
Normal file
5
app/views/api/children/index.json.jbuilder
Normal file
@ -0,0 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.array! @children do |child|
|
||||
json.partial! 'child', child: child
|
||||
end
|
3
app/views/api/children/show.json.jbuilder
Normal file
3
app/views/api/children/show.json.jbuilder
Normal file
@ -0,0 +1,3 @@
|
||||
# forzen_string_literal: true
|
||||
|
||||
json.partial! 'child', child: @child
|
3
app/views/api/children/update.json.jbuilder
Normal file
3
app/views/api/children/update.json.jbuilder
Normal file
@ -0,0 +1,3 @@
|
||||
# forzen_string_literal: true
|
||||
|
||||
json.partial! 'child', child: @child
|
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