mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2024-11-28 09:24:24 +01:00
Merge branch 'pre_inscription' into family_compte_pre_inscription
This commit is contained in:
commit
12b1ff5f0e
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).where('birthday >= ?', 18.years.ago).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 ChildService.update(@child, 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,8 @@ 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,
|
||||
:pre_registration, :pre_registration_end_date,
|
||||
event_theme_ids: [],
|
||||
event_image_attributes: %i[id attachment],
|
||||
event_files_attributes: %i[id attachment _destroy],
|
||||
|
@ -4,7 +4,7 @@
|
||||
# Reservations are used for Training, Machine, Space and Event
|
||||
class API::ReservationsController < API::APIController
|
||||
before_action :authenticate_user!
|
||||
before_action :set_reservation, only: %i[show update]
|
||||
before_action :set_reservation, only: %i[show update confirm_payment]
|
||||
respond_to :json
|
||||
|
||||
def index
|
||||
@ -34,6 +34,16 @@ class API::ReservationsController < API::APIController
|
||||
end
|
||||
end
|
||||
|
||||
def confirm_payment
|
||||
authorize @reservation
|
||||
invoice = ReservationConfirmPaymentService.new(@reservation, current_user, params[:coupon_code], params[:offered]).call
|
||||
if invoice
|
||||
render :show, status: :ok, location: @reservation
|
||||
else
|
||||
render json: @reservation.errors, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_reservation
|
||||
|
@ -5,7 +5,7 @@
|
||||
# availability by Availability.slot_duration, or otherwise globally by Setting.get('slot_duration')
|
||||
class API::SlotsReservationsController < API::APIController
|
||||
before_action :authenticate_user!
|
||||
before_action :set_slots_reservation, only: %i[update cancel]
|
||||
before_action :set_slots_reservation, only: %i[update cancel validate invalidate]
|
||||
respond_to :json
|
||||
|
||||
def update
|
||||
@ -23,6 +23,16 @@ class API::SlotsReservationsController < API::APIController
|
||||
SlotsReservationsService.cancel(@slot_reservation)
|
||||
end
|
||||
|
||||
def validate
|
||||
authorize @slot_reservation
|
||||
SlotsReservationsService.validate(@slot_reservation)
|
||||
end
|
||||
|
||||
def invalidate
|
||||
authorize @slot_reservation
|
||||
SlotsReservationsService.invalidate(@slot_reservation)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_slots_reservation
|
||||
|
@ -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,9 @@ 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);
|
||||
const [isAcitvePreRegistration, setIsActivePreRegistration] = useState<boolean>(event?.pre_registration);
|
||||
const [submitting, setSubmitting] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
EventCategoryAPI.index()
|
||||
@ -69,6 +72,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(() => {
|
||||
@ -97,6 +101,11 @@ export const EventForm: React.FC<EventFormProps> = ({ action, event, onError, on
|
||||
* Callback triggered when the user validates the machine form: handle create or update
|
||||
*/
|
||||
const onSubmit: SubmitHandler<Event> = (data: Event) => {
|
||||
setSubmitting(true);
|
||||
if (submitting) return;
|
||||
if (data.pre_registration_end_date?.toString() === 'Invalid Date') {
|
||||
data.pre_registration_end_date = null;
|
||||
}
|
||||
if (action === 'update') {
|
||||
if (event?.recurrence_events?.length > 0) {
|
||||
setUpdatingEvent(data);
|
||||
@ -108,7 +117,7 @@ export const EventForm: React.FC<EventFormProps> = ({ action, event, onError, on
|
||||
EventAPI.create(data).then(res => {
|
||||
onSuccess(t(`app.admin.event_form.${action}_success`));
|
||||
window.location.href = `/#!/events/${res.id}`;
|
||||
}).catch(onError);
|
||||
}).catch(onError).finally(() => setSubmitting(false));
|
||||
}
|
||||
};
|
||||
|
||||
@ -168,11 +177,25 @@ 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>
|
||||
<h2>{t('app.admin.event_form.ACTION_title', { ACTION: action })}</h2>
|
||||
<FabButton onClick={handleSubmit(onSubmit)} className="fab-button save-btn is-main">
|
||||
<FabButton onClick={handleSubmit(onSubmit)} disabled={submitting} className="fab-button save-btn is-main">
|
||||
{t('app.admin.event_form.save')}
|
||||
</FabButton>
|
||||
</header>
|
||||
@ -203,6 +226,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}
|
||||
@ -219,6 +248,19 @@ export const EventForm: React.FC<EventFormProps> = ({ action, event, onError, on
|
||||
formState={formState}
|
||||
options={ageRangeOptions}
|
||||
label={t('app.admin.event_form.age_range')} />}
|
||||
<FormSwitch control={control}
|
||||
id="pre_registration"
|
||||
label={t('app.admin.event_form.pre_registration')}
|
||||
formState={formState}
|
||||
tooltip={t('app.admin.event_form.pre_registration_help')}
|
||||
onChange={setIsActivePreRegistration} />
|
||||
{isAcitvePreRegistration &&
|
||||
<FormInput id="pre_registration_end_date"
|
||||
type="date"
|
||||
register={register}
|
||||
formState={formState}
|
||||
label={t('app.admin.event_form.pre_registration_end_date')} />
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
@ -0,0 +1,109 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { react2angular } from 'react2angular';
|
||||
import _ from 'lodash';
|
||||
import { Reservation } from '../../models/reservation';
|
||||
import FormatLib from '../../lib/format';
|
||||
import { IApplication } from '../../models/application';
|
||||
|
||||
declare const Application: IApplication;
|
||||
|
||||
interface EventReservationItemProps {
|
||||
reservation: Reservation;
|
||||
}
|
||||
|
||||
/**
|
||||
* event reservation item component
|
||||
*/
|
||||
export const EventReservationItem: React.FC<EventReservationItemProps> = ({ reservation }) => {
|
||||
const { t } = useTranslation('logged');
|
||||
|
||||
/**
|
||||
* Return the formatted localized date of the event
|
||||
*/
|
||||
const formatDate = (): string => {
|
||||
return `${FormatLib.date(reservation.start_at)} ${FormatLib.time(reservation.start_at)} - ${FormatLib.time(reservation.end_at)}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Build the ticket for event price category user reservation
|
||||
*/
|
||||
const buildTicket = (ticket) => {
|
||||
return (
|
||||
<>
|
||||
<label>{t('app.logged.event_reservation_item.NUMBER_of_NAME_places_reserved', { NUMBER: ticket.booked, NAME: ticket.price_category.name })}</label>
|
||||
{reservation.booking_users_attributes.filter(u => u.event_price_category_id === ticket.event_price_category_id).map(u => {
|
||||
return (
|
||||
<p key={u.id} className='name'>{u.name}</p>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the pre-registration status
|
||||
*/
|
||||
const preRegistrationStatus = () => {
|
||||
if (!_.isBoolean(reservation.is_valid) && !reservation.canceled_at && !reservation.is_paid) {
|
||||
return t('app.logged.event_reservation_item.in_the_process_of_validation');
|
||||
} else if (reservation.is_valid && !reservation.canceled_at && !reservation.is_paid && reservation.amount !== 0) {
|
||||
return t('app.logged.event_reservation_item.settle_your_payment');
|
||||
} else if (reservation.is_valid && !reservation.canceled_at && !reservation.is_paid && reservation.amount === 0) {
|
||||
return t('app.logged.event_reservation_item.registered');
|
||||
} else if (!reservation.is_valid && !reservation.canceled_at) {
|
||||
return t('app.logged.event_reservation_item.not_validated');
|
||||
} else if (reservation.is_paid && !reservation.canceled_at && reservation.amount !== 0) {
|
||||
return t('app.logged.event_reservation_item.paid');
|
||||
} else if (reservation.is_paid && !reservation.canceled_at && reservation.amount === 0) {
|
||||
return t('app.logged.event_reservation_item.present');
|
||||
} else if (reservation.canceled_at) {
|
||||
return t('app.logged.event_reservation_item.canceled');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="event-reservation-item">
|
||||
<div className="event-reservation-item__event">
|
||||
<div className="infos">
|
||||
<label>{t('app.logged.event_reservation_item.event')}</label>
|
||||
<p>{reservation.event_title}</p>
|
||||
<span className='date'>{formatDate()}</span>
|
||||
</div>
|
||||
<div className="types">
|
||||
{/* {reservation.event_type === 'family' &&
|
||||
<span className="">{t('app.logged.event_reservation_item.family')}</span>
|
||||
}
|
||||
{reservation.event_type === 'nominative' &&
|
||||
<span className="">{t('app.logged.event_reservation_item.nominative')}</span>
|
||||
} */}
|
||||
{reservation.event_pre_registration &&
|
||||
// eslint-disable-next-line fabmanager/no-bootstrap, fabmanager/no-utilities
|
||||
<span className="badge text-xs bg-info">{t('app.logged.event_reservation_item.pre_registration')}</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className="event-reservation-item__reservation">
|
||||
<div className='list'>
|
||||
<label>{t('app.logged.event_reservation_item.NUMBER_normal_places_reserved', { NUMBER: reservation.nb_reserve_places })}</label>
|
||||
{reservation.booking_users_attributes.filter(u => !u.event_price_category_id).map(u => {
|
||||
return (
|
||||
<p key={u.id} className='name'>{u.name}</p>
|
||||
);
|
||||
})}
|
||||
{reservation.tickets.map(ticket => {
|
||||
return buildTicket(ticket);
|
||||
})}
|
||||
</div>
|
||||
{reservation.event_pre_registration &&
|
||||
<div className='status'>
|
||||
<label>{t('app.logged.event_reservation_item.tracking_your_reservation')}</label>
|
||||
<p className="">{preRegistrationStatus()}</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Application.Components.component('eventReservationItem', react2angular(EventReservationItem, ['reservation']));
|
@ -0,0 +1,181 @@
|
||||
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() && supportingDocumentsTypes?.length > 0 && <>
|
||||
<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"
|
||||
rules={{ required: !sf.attachment }}
|
||||
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() && supportingDocumentsTypes?.length > 0 && <>
|
||||
<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() && supportingDocumentsTypes?.length > 0 && <>
|
||||
<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,65 @@
|
||||
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> => {
|
||||
let c: Child = data;
|
||||
try {
|
||||
if (child?.id) {
|
||||
c = await ChildAPI.update(data);
|
||||
} else {
|
||||
c = await ChildAPI.create(data);
|
||||
}
|
||||
toggleModal();
|
||||
onSuccess(c, '');
|
||||
} 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={(msg) => onSuccess(child, msg)}
|
||||
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: (child: Child, msg: 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((c) => {
|
||||
onSuccess(c, 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>}
|
||||
|
@ -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,13 +63,15 @@ export const SupportingDocumentsTypeForm: React.FC<SupportingDocumentsTypeFormPr
|
||||
{t('app.admin.settings.account.supporting_documents_type_form.type_form_info')}
|
||||
</div>
|
||||
<form name="supportingDocumentTypeForm">
|
||||
<div className="field">
|
||||
<Select defaultValue={groupsValues()}
|
||||
placeholder={t('app.admin.settings.account.supporting_documents_type_form.select_group')}
|
||||
onChange={handleGroupsChange}
|
||||
options={buildOptions()}
|
||||
isMulti />
|
||||
</div>
|
||||
{supportingDocumentType?.document_type === 'User' &&
|
||||
<div className="field">
|
||||
<Select defaultValue={groupsValues()}
|
||||
placeholder={t('app.admin.settings.account.supporting_documents_type_form.select_group')}
|
||||
onChange={handleGroupsChange}
|
||||
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,83 +192,138 @@ const SupportingDocumentsTypesList: React.FC<SupportingDocumentsTypesListProps>
|
||||
window.location.href = '/#!/admin/members?tabs=1';
|
||||
};
|
||||
|
||||
return (
|
||||
<FabPanel className="supporting-documents-types-list" header={<div>
|
||||
<span>{t('app.admin.settings.account.supporting_documents_types_list.add_supporting_documents_types')}</span>
|
||||
</div>}>
|
||||
<div className="types-list">
|
||||
<div className="groups">
|
||||
<p>{t('app.admin.settings.account.supporting_documents_types_list.supporting_documents_type_info')}</p>
|
||||
<FabAlert level="warning">
|
||||
<HtmlTranslate trKey="app.admin.settings.account.supporting_documents_types_list.no_groups_info" />
|
||||
<FabButton onClick={addGroup}>{t('app.admin.settings.account.supporting_documents_types_list.create_groups')}</FabButton>
|
||||
</FabAlert>
|
||||
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>
|
||||
</div>}>
|
||||
<div className="types-list">
|
||||
<div className="groups">
|
||||
<p>{t('app.admin.settings.account.supporting_documents_types_list.supporting_documents_type_info')}</p>
|
||||
<FabAlert level="warning">
|
||||
<HtmlTranslate trKey="app.admin.settings.account.supporting_documents_types_list.no_groups_info" />
|
||||
<FabButton onClick={addGroup}>{t('app.admin.settings.account.supporting_documents_types_list.create_groups')}</FabButton>
|
||||
</FabAlert>
|
||||
</div>
|
||||
|
||||
<div className="title">
|
||||
<h3>{t('app.admin.settings.account.supporting_documents_types_list.supporting_documents_type_title')}</h3>
|
||||
<FabButton onClick={addType}>{t('app.admin.settings.account.supporting_documents_types_list.add_type')}</FabButton>
|
||||
</div>
|
||||
|
||||
<SupportingDocumentsTypeModal isOpen={modalIsOpen}
|
||||
groups={groups}
|
||||
proofOfIdentityType={supportingDocumentsType}
|
||||
documentType={documentType}
|
||||
toggleModal={toggleCreateAndEditModal}
|
||||
onSuccess={onSaveTypeSuccess}
|
||||
onError={onError} />
|
||||
<DeleteSupportingDocumentsTypeModal isOpen={destroyModalIsOpen}
|
||||
proofOfIdentityTypeId={supportingDocumentsTypeId}
|
||||
toggleModal={toggleDestroyModal}
|
||||
onSuccess={onDestroySuccess}
|
||||
onError={onError}/>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="group-name">
|
||||
<a onClick={setTypeOrder('group_name')}>
|
||||
{t('app.admin.settings.account.supporting_documents_types_list.group_name')}
|
||||
<i className={orderClassName('group_name')} />
|
||||
</a>
|
||||
</th>
|
||||
<th className="name">
|
||||
<a onClick={setTypeOrder('name')}>
|
||||
{t('app.admin.settings.account.supporting_documents_types_list.name')}
|
||||
<i className={orderClassName('name')} />
|
||||
</a>
|
||||
</th>
|
||||
<th className="actions"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{supportingDocumentsTypes.map(poit => {
|
||||
return (
|
||||
<tr key={poit.id}>
|
||||
<td>{getGroupsNames(poit.group_ids)}</td>
|
||||
<td>{poit.name}</td>
|
||||
<td>
|
||||
<div className="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>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
{!hasTypes() && (
|
||||
<p className="no-types-info">
|
||||
<HtmlTranslate trKey="app.admin.settings.account.supporting_documents_types_list.no_types" />
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</FabPanel>
|
||||
);
|
||||
} 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>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<SupportingDocumentsTypeModal isOpen={modalIsOpen}
|
||||
groups={groups}
|
||||
proofOfIdentityType={supportingDocumentsType}
|
||||
toggleModal={toggleCreateAndEditModal}
|
||||
onSuccess={onSaveTypeSuccess}
|
||||
onError={onError} />
|
||||
<DeleteSupportingDocumentsTypeModal isOpen={destroyModalIsOpen}
|
||||
proofOfIdentityTypeId={supportingDocumentsTypeId}
|
||||
toggleModal={toggleDestroyModal}
|
||||
onSuccess={onDestroySuccess}
|
||||
onError={onError}/>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="group-name">
|
||||
<a onClick={setTypeOrder('group_name')}>
|
||||
{t('app.admin.settings.account.supporting_documents_types_list.group_name')}
|
||||
<i className={orderClassName('group_name')} />
|
||||
</a>
|
||||
</th>
|
||||
<th className="name">
|
||||
<a onClick={setTypeOrder('name')}>
|
||||
{t('app.admin.settings.account.supporting_documents_types_list.name')}
|
||||
<i className={orderClassName('name')} />
|
||||
</a>
|
||||
</th>
|
||||
<th className="actions"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{supportingDocumentsTypes.map(poit => {
|
||||
return (
|
||||
<tr key={poit.id}>
|
||||
<td>{getGroupsNames(poit.group_ids)}</td>
|
||||
<td>{poit.name}</td>
|
||||
<td>
|
||||
<div className="buttons">
|
||||
<FabButton className="edit-btn" onClick={editType(poit)}>
|
||||
<i className="fa fa-edit" />
|
||||
</FabButton>
|
||||
<FabButton className="delete-btn" onClick={destroyType(poit.id)}>
|
||||
<i className="fa fa-trash" />
|
||||
</FabButton>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
{!hasTypes() && (
|
||||
<p className="no-types-info">
|
||||
<HtmlTranslate trKey="app.admin.settings.account.supporting_documents_types_list.no_types" />
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</FabPanel>
|
||||
);
|
||||
);
|
||||
} 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,104 @@
|
||||
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`;
|
||||
};
|
||||
|
||||
/**
|
||||
* member and all his children are validated
|
||||
*/
|
||||
const memberIsValidated = (): boolean => {
|
||||
return member.validated_at && member.children.every((child) => child.validated_at);
|
||||
};
|
||||
|
||||
return (
|
||||
<div key={member.id} className={`members-list-item ${memberIsValidated() ? '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']));
|
@ -436,7 +436,7 @@ Application.Controllers.controller('AdminEventsController', ['$scope', '$state',
|
||||
/**
|
||||
* Controller used in the reservations listing page for a specific event
|
||||
*/
|
||||
Application.Controllers.controller('ShowEventReservationsController', ['$scope', 'eventPromise', 'reservationsPromise', function ($scope, eventPromise, reservationsPromise) {
|
||||
Application.Controllers.controller('ShowEventReservationsController', ['$scope', 'eventPromise', 'reservationsPromise', 'dialogs', 'SlotsReservation', 'growl', '_t', 'Price', 'Wallet', '$uibModal', 'Event', function ($scope, eventPromise, reservationsPromise, dialogs, SlotsReservation, growl, _t, Price, Wallet, $uibModal, Event) {
|
||||
// retrieve the event from the ID provided in the current URL
|
||||
$scope.event = eventPromise;
|
||||
|
||||
@ -451,6 +451,219 @@ Application.Controllers.controller('ShowEventReservationsController', ['$scope',
|
||||
$scope.isCancelled = function (reservation) {
|
||||
return !!(reservation.slots_reservations_attributes[0].canceled_at);
|
||||
};
|
||||
|
||||
/**
|
||||
* Test if the provided reservation has been validated
|
||||
* @param reservation {Reservation}
|
||||
* @returns {boolean}
|
||||
*/
|
||||
$scope.isValidated = function (reservation) {
|
||||
return reservation.slots_reservations_attributes[0].is_valid === true || reservation.slots_reservations_attributes[0].is_valid === 'true';
|
||||
};
|
||||
|
||||
/**
|
||||
* Test if the provided reservation has been invalidated
|
||||
* @param reservation {Reservation}
|
||||
* @returns {boolean}
|
||||
*/
|
||||
$scope.isInvalidated = function (reservation) {
|
||||
return reservation.slots_reservations_attributes[0].is_valid === false || reservation.slots_reservations_attributes[0].is_valid === 'false';
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the price of a reservation
|
||||
* @param reservation {Reservation}
|
||||
*/
|
||||
$scope.reservationAmount = function (reservation) {
|
||||
let amount = 0;
|
||||
for (const user of reservation.booking_users_attributes) {
|
||||
if (user.event_price_category_id) {
|
||||
const price_category = _.find($scope.event.event_price_categories_attributes, { id: user.event_price_category_id });
|
||||
if (price_category) {
|
||||
amount += price_category.amount;
|
||||
}
|
||||
} else {
|
||||
amount += $scope.event.amount;
|
||||
}
|
||||
}
|
||||
return amount;
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback to validate a reservation
|
||||
* @param reservation {Reservation}
|
||||
*/
|
||||
$scope.validateReservation = function (reservation) {
|
||||
SlotsReservation.validate({
|
||||
id: reservation.slots_reservations_attributes[0].id
|
||||
}, () => { // successfully validated
|
||||
growl.success(_t('app.admin.event_reservations.reservation_was_successfully_validated'));
|
||||
const index = $scope.reservations.indexOf(reservation);
|
||||
$scope.reservations[index].slots_reservations_attributes[0].is_valid = true;
|
||||
Event.get({ id: $scope.event.id }).$promise.then(function (event) {
|
||||
$scope.event = event;
|
||||
});
|
||||
}, () => {
|
||||
growl.warning(_t('app.admin.event_reservations.validation_failed'));
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback to invalidate a reservation
|
||||
* @param reservation {Reservation}
|
||||
*/
|
||||
$scope.invalidateReservation = function (reservation) {
|
||||
SlotsReservation.invalidate({
|
||||
id: reservation.slots_reservations_attributes[0].id
|
||||
}, () => { // successfully validated
|
||||
growl.success(_t('app.admin.event_reservations.reservation_was_successfully_invalidated'));
|
||||
const index = $scope.reservations.indexOf(reservation);
|
||||
$scope.reservations[index].slots_reservations_attributes[0].is_valid = false;
|
||||
Event.get({ id: $scope.event.id }).$promise.then(function (event) {
|
||||
$scope.event = event;
|
||||
});
|
||||
}, () => {
|
||||
growl.warning(_t('app.admin.event_reservations.invalidation_failed'));
|
||||
});
|
||||
};
|
||||
|
||||
const mkCartItems = function (reservation, coupon) {
|
||||
return {
|
||||
customer_id: reservation.user_id,
|
||||
items: [{
|
||||
reservation: {
|
||||
...reservation,
|
||||
slots_reservations_attributes: reservation.slots_reservations_attributes.map(sr => ({ slot_id: sr.slot_id })),
|
||||
tickets_attributes: reservation.tickets_attributes.map(t => ({ booked: t.booked, event_price_category_id: t.event_price_category.id })),
|
||||
booking_users_attributes: reservation.booking_users_attributes.map(bu => (
|
||||
{ name: bu.name, event_price_category_id: bu.event_price_category_id, booked_id: bu.booked_id, booked_type: bu.booked_type }
|
||||
))
|
||||
}
|
||||
}],
|
||||
coupon_code: ((coupon ? coupon.code : undefined)),
|
||||
payment_method: ''
|
||||
};
|
||||
};
|
||||
|
||||
$scope.payReservation = function (reservation) {
|
||||
const modalInstance = $uibModal.open({
|
||||
templateUrl: '/admin/events/pay_reservation_modal.html',
|
||||
size: 'sm',
|
||||
resolve: {
|
||||
event () {
|
||||
return $scope.event;
|
||||
},
|
||||
reservation () {
|
||||
return reservation;
|
||||
},
|
||||
price () {
|
||||
return Price.compute(mkCartItems(reservation)).$promise;
|
||||
},
|
||||
wallet () {
|
||||
return Wallet.getWalletByUser({ user_id: reservation.user_id }).$promise;
|
||||
},
|
||||
cartItems () {
|
||||
return mkCartItems(reservation);
|
||||
}
|
||||
},
|
||||
controller: ['$scope', '$uibModalInstance', 'reservation', 'price', 'wallet', 'cartItems', 'helpers', '$filter', '_t', 'Reservation', 'event',
|
||||
function ($scope, $uibModalInstance, reservation, price, wallet, cartItems, helpers, $filter, _t, Reservation, event) {
|
||||
$scope.event = event;
|
||||
|
||||
// User's wallet amount
|
||||
$scope.wallet = wallet;
|
||||
|
||||
// Price
|
||||
$scope.price = price;
|
||||
|
||||
// Cart items
|
||||
$scope.cartItems = cartItems;
|
||||
|
||||
// price to pay
|
||||
$scope.amount = helpers.getAmountToPay(price.price, wallet.amount);
|
||||
|
||||
// Reservation
|
||||
$scope.reservation = reservation;
|
||||
|
||||
$scope.coupon = { applied: null };
|
||||
|
||||
$scope.offered = false;
|
||||
|
||||
$scope.payment = false;
|
||||
|
||||
// Button label
|
||||
$scope.setValidButtonName = function () {
|
||||
if ($scope.amount > 0 && !$scope.offered) {
|
||||
$scope.validButtonName = _t('app.admin.event_reservations.confirm_payment_of_html', { ROLE: $scope.currentUser.role, AMOUNT: $filter('currency')($scope.amount) });
|
||||
} else {
|
||||
$scope.validButtonName = _t('app.shared.buttons.confirm');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Compute the total amount for the current reservation according to the previously set parameters
|
||||
*/
|
||||
$scope.computeEventAmount = function () {
|
||||
Price.compute(mkCartItems(reservation, $scope.coupon.applied), function (res) {
|
||||
$scope.price = res;
|
||||
$scope.amount = helpers.getAmountToPay($scope.price.price, wallet.amount);
|
||||
$scope.setValidButtonName();
|
||||
});
|
||||
};
|
||||
|
||||
// Callback to validate the payment
|
||||
$scope.ok = function () {
|
||||
$scope.attempting = true;
|
||||
return Reservation.confirm_payment({
|
||||
id: reservation.id,
|
||||
coupon_code: $scope.coupon.applied ? $scope.coupon.applied.code : null,
|
||||
offered: $scope.offered
|
||||
}, function (res) {
|
||||
$uibModalInstance.close(res);
|
||||
return $scope.attempting = true;
|
||||
}
|
||||
, function (response) {
|
||||
$scope.alerts = [];
|
||||
angular.forEach(response, function (v, k) {
|
||||
angular.forEach(v, function (err) {
|
||||
$scope.alerts.push({
|
||||
msg: k + ': ' + err,
|
||||
type: 'danger'
|
||||
});
|
||||
});
|
||||
});
|
||||
return $scope.attempting = false;
|
||||
});
|
||||
};
|
||||
|
||||
// Callback to cancel the payment
|
||||
$scope.cancel = function () { $uibModalInstance.dismiss('cancel'); };
|
||||
|
||||
$scope.$watch('coupon.applied', function (newValue, oldValue) {
|
||||
if ((newValue !== null) || (oldValue !== null)) {
|
||||
return $scope.computeEventAmount();
|
||||
}
|
||||
});
|
||||
|
||||
$scope.setValidButtonName();
|
||||
}]
|
||||
});
|
||||
modalInstance.result.then(function (reservation) {
|
||||
$scope.reservations = $scope.reservations.map((r) => {
|
||||
if (r.id === reservation.id) {
|
||||
return reservation;
|
||||
}
|
||||
if ($scope.reservationAmount(reservation) === 0) {
|
||||
growl.success(_t('app.admin.event_reservations.reservation_was_successfully_present'));
|
||||
} else {
|
||||
growl.success(_t('app.admin.event_reservations.reservation_was_successfully_paid'));
|
||||
}
|
||||
return r;
|
||||
});
|
||||
}, function () {
|
||||
console.log('Pay reservation modal dismissed at: ' + new Date());
|
||||
});
|
||||
};
|
||||
}]);
|
||||
|
||||
/**
|
||||
|
@ -293,7 +293,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) {
|
||||
@ -305,6 +305,31 @@ 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;
|
||||
});
|
||||
return member;
|
||||
}
|
||||
return member;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Ask for confirmation then delete the specified administrator
|
||||
* @param admins {Array} full list of administrators
|
||||
@ -590,6 +615,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 */
|
||||
|
||||
/**
|
||||
|
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,23 @@ 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) {
|
||||
let reservedPlaces = 0;
|
||||
if ($scope.event.event_type === 'family') {
|
||||
const reservations = $scope.reservations.filter((reservation) => {
|
||||
return !reservation.slots_reservations_attributes[0].canceled_at;
|
||||
});
|
||||
reservedPlaces = reservations.reduce((sum, reservation) => {
|
||||
return sum + reservation.booking_users_attributes.length;
|
||||
}, 0);
|
||||
}
|
||||
let nb_free_places = $scope.event.nb_free_places;
|
||||
if ($scope.event.event_type === 'family') {
|
||||
const maxPlaces = $scope.children.length + 1 - reservedPlaces;
|
||||
nb_free_places = Math.min(maxPlaces, $scope.event.nb_free_places);
|
||||
}
|
||||
// compute the total remaining places
|
||||
let remain = $scope.event.nb_free_places - $scope.reserve.nbReservePlaces;
|
||||
let remain = nb_free_places - $scope.reserve.nbReservePlaces;
|
||||
for (let ticket in $scope.reserve.tickets) {
|
||||
remain -= $scope.reserve.tickets[ticket];
|
||||
}
|
||||
@ -247,17 +270,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 = () => {
|
||||
@ -265,10 +312,16 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
|
||||
};
|
||||
|
||||
$scope.isShowReserveEventButton = () => {
|
||||
return $scope.event.nb_free_places > 0 &&
|
||||
const bookable = $scope.event.nb_free_places > 0 &&
|
||||
!$scope.reserve.toReserve &&
|
||||
$scope.now.isBefore($scope.eventEndDateTime) &&
|
||||
helpers.isUserValidatedByType($scope.ctrl.member, $scope.settings, 'event');
|
||||
if ($scope.event.pre_registration) {
|
||||
const endDate = $scope.event.pre_registration_end_date || $scope.event.end_date
|
||||
return bookable && $scope.now.isSameOrBefore(endDate, 'day');
|
||||
} else {
|
||||
return bookable;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@ -321,7 +374,11 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
|
||||
if ($scope.ctrl.member) {
|
||||
Member.get({ id: $scope.ctrl.member.id }, function (member) {
|
||||
$scope.ctrl.member = member;
|
||||
getReservations($scope.event.id, 'Event', $scope.ctrl.member.id);
|
||||
getReservations($scope.event.id, 'Event', $scope.ctrl.member.id).then(function () {
|
||||
getChildren($scope.ctrl.member.id).then(function (children) {
|
||||
updateNbReservePlaces();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -399,8 +456,10 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
|
||||
let index;
|
||||
growl.success(_t('app.public.events_show.reservation_was_successfully_cancelled'));
|
||||
index = $scope.reservations.indexOf(reservation);
|
||||
$scope.event.nb_free_places = $scope.event.nb_free_places + reservation.total_booked_seats;
|
||||
$scope.reservations[index].slots_reservations_attributes[0].canceled_at = new Date();
|
||||
Event.get({ id: $scope.event.id }).$promise.then(function (event) {
|
||||
$scope.event = event;
|
||||
});
|
||||
}, function(error) {
|
||||
growl.warning(_t('app.public.events_show.cancellation_failed'));
|
||||
});
|
||||
@ -583,6 +642,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.validated_at) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
$scope.isUnder18YearsAgo = (date) => {
|
||||
return moment(date).isAfter(moment().subtract(18, 'year'));
|
||||
}
|
||||
|
||||
/* PRIVATE SCOPE */
|
||||
|
||||
/**
|
||||
@ -601,7 +692,11 @@ 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);
|
||||
getReservations($scope.event.id, 'Event', $scope.currentUser.id).then(function () {
|
||||
getChildren($scope.currentUser.id).then(function (children) {
|
||||
updateNbReservePlaces();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// watch when a coupon is applied to re-compute the total price
|
||||
@ -619,13 +714,98 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
|
||||
* @param user_id {number} the user's id (current or managed)
|
||||
*/
|
||||
const getReservations = function (reservable_id, reservable_type, user_id) {
|
||||
Reservation.query({
|
||||
return Reservation.query({
|
||||
reservable_id,
|
||||
reservable_type,
|
||||
user_id
|
||||
}).$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;
|
||||
}
|
||||
}
|
||||
}
|
||||
const reservations = $scope.reservations.filter((reservation) => {
|
||||
return !reservation.slots_reservations_attributes[0].canceled_at;
|
||||
});
|
||||
for (const r of reservations) {
|
||||
for (const user of r.booking_users_attributes) {
|
||||
const key = user.booked_type === 'User' ? `user_${user.booked_id}` : `child_${user.booked_id}`;
|
||||
if (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',
|
||||
validated_at: 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' && $scope.ctrl.member.id) {
|
||||
const reservations = $scope.reservations.filter((reservation) => {
|
||||
return !reservation.slots_reservations_attributes[0].canceled_at;
|
||||
});
|
||||
const reservedPlaces = reservations.reduce((sum, reservation) => {
|
||||
return sum + reservation.booking_users_attributes.length;
|
||||
}, 0);
|
||||
const maxPlaces = $scope.children.length + 1 - reservedPlaces;
|
||||
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 +818,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 +837,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 +882,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;
|
||||
}
|
||||
|
||||
@ -810,13 +1008,15 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
|
||||
* @param invoice {Object} the invoice for the booked reservation
|
||||
*/
|
||||
const afterPayment = function (invoice) {
|
||||
Reservation.get({ id: invoice.main_object.id }, function (reservation) {
|
||||
$scope.event.nb_free_places = $scope.event.nb_free_places - reservation.total_booked_seats;
|
||||
$scope.reservations.push(reservation);
|
||||
Event.get({ id: $scope.event.id }).$promise.then(function (event) {
|
||||
$scope.event = event;
|
||||
getReservations($scope.event.id, 'Event', $scope.ctrl.member.id).then(function () {
|
||||
updateNbReservePlaces();
|
||||
$scope.reserveSuccess = true;
|
||||
$scope.coupon.applied = null;
|
||||
});
|
||||
resetEventReserve();
|
||||
});
|
||||
resetEventReserve();
|
||||
$scope.reserveSuccess = true;
|
||||
$scope.coupon.applied = null;
|
||||
if ($scope.currentUser.role === 'admin') {
|
||||
return $scope.ctrl.member = null;
|
||||
}
|
||||
|
@ -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,10 @@ export interface Event {
|
||||
}>,
|
||||
recurrence: RecurrenceOption,
|
||||
recurrence_end_at: Date,
|
||||
advanced_accounting_attributes?: AdvancedAccounting
|
||||
advanced_accounting_attributes?: AdvancedAccounting,
|
||||
event_type: EventType,
|
||||
pre_registration?: boolean,
|
||||
pre_registration_end_date?: TDateISODate | Date,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
]
|
||||
}
|
@ -27,10 +27,11 @@ export interface Reservation {
|
||||
slots_reservations_attributes: Array<SlotsReservation>,
|
||||
reservable?: {
|
||||
id: number,
|
||||
name: string
|
||||
name: string,
|
||||
amount?: number
|
||||
},
|
||||
nb_reserve_places?: number,
|
||||
tickets_attributes?: {
|
||||
tickets_attributes?: Array<{
|
||||
event_price_category_id: number,
|
||||
event_price_category?: {
|
||||
id: number,
|
||||
@ -40,11 +41,40 @@ export interface Reservation {
|
||||
name: string
|
||||
}
|
||||
},
|
||||
booked: boolean,
|
||||
booked: number,
|
||||
created_at?: TDateISO
|
||||
},
|
||||
}>,
|
||||
tickets?: Array<{
|
||||
event_price_category_id: number,
|
||||
event_price_category?: {
|
||||
id: number,
|
||||
price_category_id: number,
|
||||
price_category: {
|
||||
id: number,
|
||||
name: string
|
||||
}
|
||||
},
|
||||
booked: number,
|
||||
created_at?: TDateISO
|
||||
}>,
|
||||
total_booked_seats?: number,
|
||||
created_at?: TDateISO,
|
||||
booking_users_attributes?: Array<{
|
||||
id: number,
|
||||
name: string,
|
||||
event_price_category_id: number,
|
||||
booked_id: number,
|
||||
booked_type: string,
|
||||
}>,
|
||||
start_at: TDateISO,
|
||||
end_at: TDateISO,
|
||||
event_type?: string,
|
||||
event_title?: string,
|
||||
event_pre_registration?: boolean,
|
||||
canceled_at?: TDateISO,
|
||||
is_valid?: boolean,
|
||||
is_paid?: boolean,
|
||||
amount?: number
|
||||
}
|
||||
|
||||
export interface ReservationIndexFilter extends ApiFilter {
|
||||
|
@ -179,7 +179,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: {
|
||||
@ -624,7 +636,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'
|
||||
}
|
||||
}
|
||||
);
|
||||
}]);
|
@ -5,6 +5,11 @@ Application.Services.factory('Reservation', ['$resource', function ($resource) {
|
||||
{ id: '@id' }, {
|
||||
update: {
|
||||
method: 'PUT'
|
||||
},
|
||||
confirm_payment: {
|
||||
method: 'POST',
|
||||
url: '/api/reservations/confirm_payment',
|
||||
isArray: false
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@ -9,6 +9,14 @@ Application.Services.factory('SlotsReservation', ['$resource', function ($resour
|
||||
cancel: {
|
||||
method: 'PUT',
|
||||
url: '/api/slots_reservations/:id/cancel'
|
||||
},
|
||||
validate: {
|
||||
method: 'PUT',
|
||||
url: '/api/slots_reservations/:id/validate'
|
||||
},
|
||||
invalidate: {
|
||||
method: 'PUT',
|
||||
url: '/api/slots_reservations/:id/invalidate'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@ -48,11 +48,16 @@
|
||||
@import "modules/dashboard/reservations/prepaid-packs-panel";
|
||||
@import "modules/dashboard/reservations/reservations-dashboard";
|
||||
@import "modules/dashboard/reservations/reservations-panel";
|
||||
@import "modules/events/event";
|
||||
@import "modules/events/events";
|
||||
@import "modules/events/event-form";
|
||||
@import "modules/events/event-reservation";
|
||||
@import "modules/events/event";
|
||||
@import "modules/events/events-dashboard";
|
||||
@import "modules/events/events-settings";
|
||||
@import "modules/events/events";
|
||||
@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,60 @@
|
||||
.event-reservation {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.6rem;
|
||||
|
||||
&-item {
|
||||
padding: 1.6rem 1.6rem 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--gray-soft-lightest);
|
||||
border-radius: var(--border-radius);
|
||||
|
||||
label {
|
||||
margin: 0;
|
||||
@include text-xs;
|
||||
color: var(--gray-hard-light);
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
@include text-base(600);
|
||||
}
|
||||
.date { @include text-sm; }
|
||||
|
||||
&__event {
|
||||
padding-bottom: 1.2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1.6rem;
|
||||
border-bottom: 1px solid var(--gray-soft-dark);
|
||||
}
|
||||
|
||||
&__reservation {
|
||||
display: flex;
|
||||
|
||||
& > div {
|
||||
padding: 1.2rem 1.6rem 1.2rem 0;
|
||||
flex: 1;
|
||||
}
|
||||
.list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 0.5rem;
|
||||
label:not(:first-of-type) {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
.name { @include text-sm(500); }
|
||||
|
||||
.status {
|
||||
padding-left: 1.6rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
border-left: 1px solid var(--gray-soft-dark);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
.events-dashboard {
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
padding-bottom: 6rem;
|
||||
}
|
@ -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;
|
||||
|
@ -1,4 +1,109 @@
|
||||
.promote-member img {
|
||||
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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -78,4 +95,4 @@
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -12,8 +12,9 @@
|
||||
<tr>
|
||||
<th style="width:30%" translate>{{ 'app.admin.events.title' }}</th>
|
||||
<th style="width:30%" translate>{{ 'app.admin.events.dates' }}</th>
|
||||
<th style="width:10%" translate>{{ 'app.admin.events.booking' }}</th>
|
||||
<th style="width:30%"></th>
|
||||
<th style="width:15%" translate>{{ 'app.admin.events.types' }}</th>
|
||||
<th style="width:10%" translate>{{ 'app.admin.events.booking' }}</th>
|
||||
<th style="width:15%"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -48,8 +49,16 @@
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<span ng-if="event.event_type === 'standard'" class="v-middle badge text-sm bg-stage" translate="">{{ 'app.admin.events.event_type.standard' }}</span>
|
||||
<span ng-if="event.event_type === 'nominative'" class="v-middle badge text-sm bg-event" translate="">{{ 'app.admin.events.event_type.nominative' }}</span>
|
||||
<span ng-if="event.event_type === 'family'" class="v-middle badge text-sm bg-atelier" translate="">{{ 'app.admin.events.event_type.family' }}</span>
|
||||
<span ng-if="event.pre_registration" class="v-middle badge text-sm bg-info" translate="">{{ 'app.admin.events.pre_registration' }}</span>
|
||||
</td>
|
||||
|
||||
<td style="vertical-align:middle">
|
||||
<span class="ng-binding" ng-if="event.nb_total_places > 0">{{ event.nb_total_places - event.nb_free_places }} / {{ event.nb_total_places }}</span>
|
||||
<div class="ng-binding" ng-if="event.pre_registration">{{'app.admin.events.NUMBER_pre_registered' | translate:{NUMBER:event.nb_places_for_pre_registration} }}</div>
|
||||
<span class="badge font-sbold cancelled" ng-if="event.nb_total_places == -1" translate>{{ 'app.admin.events.cancelled' }}</span>
|
||||
<span class="badge font-sbold" ng-if="!event.nb_total_places" translate>{{ 'app.admin.events.without_reservation' }}</span>
|
||||
</td>
|
||||
@ -57,10 +66,10 @@
|
||||
<td style="vertical-align:middle">
|
||||
<div class="buttons">
|
||||
<a class="btn btn-default" ui-sref="app.admin.event_reservations({id: event.id})">
|
||||
<i class="fa fa-bookmark"></i> {{ 'app.admin.events.view_reservations' | translate }}
|
||||
<i class="fa fa-eye"></i>
|
||||
</a>
|
||||
<a class="btn btn-default" ui-sref="app.admin.events_edit({id: event.id})">
|
||||
<i class="fa fa-edit"></i> {{ 'app.shared.buttons.edit' | translate }}
|
||||
<i class="fa fa-edit"></i>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
|
@ -0,0 +1,52 @@
|
||||
<div class="modal-header">
|
||||
<img ng-src="{{logoBlack.custom_asset_file_attributes.attachment_url}}" alt="{{logo.custom_asset_file_attributes.attachment}}" class="modal-logo"/>
|
||||
<h1 translate ng-show="reservation && amount !== 0">{{ 'app.admin.event_reservations.confirm_payment' }}</h1>
|
||||
<h1 translate ng-show="reservation && amount === 0">{{ 'app.admin.event_reservations.confirm_present' }}</h1>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div ng-show="amount !== 0">
|
||||
<div class="row" ng-show="!offered">
|
||||
<wallet-info current-user="currentUser"
|
||||
cart="cartItems"
|
||||
price="price.price"
|
||||
wallet="wallet"/>
|
||||
</div>
|
||||
<div class="row m-b">
|
||||
<div class="col-md-12">
|
||||
<label for="offerSlot" class="control-label m-r" translate>{{ 'app.admin.event_reservations.offer_this_reservation' }}</label>
|
||||
<input bs-switch
|
||||
ng-model="offered"
|
||||
id="offerSlot"
|
||||
type="checkbox"
|
||||
class="form-control"
|
||||
switch-on-text="{{ 'app.shared.buttons.yes' | translate }}"
|
||||
switch-off-text="{{ 'app.shared.buttons.no' | translate }}"
|
||||
switch-animate="true"
|
||||
ng-change="computeEventAmount()"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<coupon show="true" coupon="coupon.applied" total="price.price_without_coupon" user-id="{{reservation.user_id}}"></coupon>
|
||||
|
||||
<div class="row">
|
||||
<div class="form-group col-sm-12">
|
||||
<div class="checkbox-group">
|
||||
<input type="checkbox"
|
||||
name="paymentReceived"
|
||||
id="paymentReceived"
|
||||
ng-model="payment" />
|
||||
<label for="paymentReceived" translate>{{ 'app.admin.event_reservations.i_have_received_the_payment' }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="amount === 0">
|
||||
<p translate>{{ 'app.admin.event_reservations.confirm_present_info' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-info" ng-if="amount !== 0" ng-click="ok()" ng-disabled="attempting || !payment" ng-bind-html="validButtonName"></button>
|
||||
<button class="btn btn-info" ng-if="amount === 0" ng-click="ok()" ng-disabled="attempting" ng-bind-html="validButtonName"></button>
|
||||
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'app.shared.buttons.cancel' }}</button>
|
||||
</div>
|
@ -20,10 +20,13 @@
|
||||
<table class="table" ng-if="reservations.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:25%" translate>{{ 'app.admin.event_reservations.user' }}</th>
|
||||
<th style="width:25%" translate>{{ 'app.admin.event_reservations.payment_date' }}</th>
|
||||
<th style="width:25%" translate>{{ 'app.admin.event_reservations.reserved_tickets' }}</th>
|
||||
<th style="width:25%"></th>
|
||||
<th translate>{{ 'app.admin.event_reservations.booked_by' }}</th>
|
||||
<th translate>{{ 'app.admin.event_reservations.reservations' }}</th>
|
||||
<th translate>{{ 'app.admin.event_reservations.date' }}</th>
|
||||
<th translate>{{ 'app.admin.event_reservations.reserved_tickets' }}</th>
|
||||
<th ng-if="event.pre_registration" translate>{{ 'app.admin.event_reservations.status' }}</th>
|
||||
<th ng-if="event.pre_registration" translate>{{ 'app.admin.event_reservations.validation' }}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -31,16 +34,49 @@
|
||||
<td class="text-c">
|
||||
<a ui-sref="app.logged.members_show({id: reservation.user_id})">{{ reservation.user_full_name }} </a>
|
||||
</td>
|
||||
<td>
|
||||
<span ng-if="event.event_type === 'standard'">{{ reservation.user_full_name }} </span>
|
||||
<div ng-repeat="bu in reservation.booking_users_attributes">
|
||||
<span>{{bu.name}}</span>
|
||||
<span ng-if="bu.booked_type === 'Child'" class="m-l-sm">({{ 'app.admin.event_reservations.age' | translate:{NUMBER: bu.age} }})</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ reservation.created_at | amDateFormat:'LL LTS' }}</td>
|
||||
<td>
|
||||
<span ng-if="reservation.nb_reserve_places > 0">{{ 'app.admin.event_reservations.full_price_' | translate }} {{reservation.nb_reserve_places}}<br/></span>
|
||||
<span ng-repeat="ticket in reservation.tickets_attributes">{{ticket.event_price_category.price_category.name}} : {{ticket.booked}}</span>
|
||||
<div ng-show="isCancelled(reservation)" class="canceled-marker" translate>{{ 'app.admin.event_reservations.canceled' }}</div>
|
||||
</td>
|
||||
<td ng-if="event.pre_registration">
|
||||
<span ng-if="!isValidated(reservation) && !isInvalidated(reservation) && !isCancelled(reservation) && !reservation.is_paid" class="v-middle badge text-sm bg-info" translate="">{{ 'app.admin.event_reservations.event_status.pre_registered' }}</span>
|
||||
<span ng-if="reservationAmount(reservation) !== 0 && isValidated(reservation) && !isCancelled(reservation) && !reservation.is_paid" class="v-middle badge text-sm bg-stage" translate="">{{ 'app.admin.event_reservations.event_status.to_pay' }}</span>
|
||||
<span ng-if="reservationAmount(reservation) === 0 && isValidated(reservation) && !isCancelled(reservation) && !reservation.is_paid" class="v-middle badge text-sm bg-stage" translate="">{{ 'app.admin.event_reservations.event_status.registered' }}</span>
|
||||
<span ng-if="isInvalidated(reservation) && !isCancelled(reservation) && !reservation.is_paid" class="v-middle badge text-sm bg-event" translate="">{{ 'app.admin.event_reservations.event_status.not_validated' }}</span>
|
||||
<span ng-if="reservationAmount(reservation) !== 0 && reservation.is_paid && !isCancelled(reservation)" class="v-middle badge text-sm bg-success" translate="">{{ 'app.admin.event_reservations.event_status.paid' }}</span>
|
||||
<span ng-if="reservationAmount(reservation) === 0 && reservation.is_paid && !isCancelled(reservation)" class="v-middle badge text-sm bg-success" translate="">{{ 'app.admin.event_reservations.event_status.present' }}</span>
|
||||
<span ng-if="isCancelled(reservation)" class="v-middle badge text-sm bg-event" translate="">{{ 'app.admin.event_reservations.event_status.canceled' }}</span>
|
||||
</td>
|
||||
<td ng-if="event.pre_registration">
|
||||
<div>
|
||||
<div ng-if="!isCancelled(reservation) && !reservation.is_paid">
|
||||
<label class="m-r-sm">
|
||||
<span translate>{{ 'app.admin.event_reservations.negative' }}</span>
|
||||
<input type="radio" name="invalidate-{{reservation.id}}" ng-value="false" ng-click="invalidateReservation(reservation)" ng-model="reservation.slots_reservations_attributes[0].is_valid" ng-disabled="reservation.total_booked_seats > event.nb_free_places && !reservation.slots_reservations_attributes[0].is_valid">
|
||||
</label>
|
||||
<label>
|
||||
<span translate>{{ 'app.admin.event_reservations.affirmative' }}</span>
|
||||
<input type="radio" name="validate-{{reservation.id}}" ng-value="true" ng-click="validateReservation(reservation)" ng-model="reservation.slots_reservations_attributes[0].is_valid" ng-disabled="reservation.total_booked_seats > event.nb_free_places && !reservation.slots_reservations_attributes[0].is_valid" >
|
||||
</label>
|
||||
</div>
|
||||
<button class="btn btn-default" ng-click="payReservation(reservation)" ng-if="isValidated(reservation) && !isCancelled(reservation) && !reservation.is_paid">
|
||||
<span ng-if="reservationAmount(reservation) !== 0" translate>{{ 'app.admin.event_reservations.pay' }}</span>
|
||||
<span ng-if="reservationAmount(reservation) === 0" translate>{{ 'app.admin.event_reservations.present' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="buttons">
|
||||
<button class="btn btn-default" ui-sref="app.public.events_show({id: event.id})">
|
||||
<i class="fa fa-tag"></i> {{ 'app.admin.event_reservations.show_the_event' | translate }}
|
||||
<i class="fa fa-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
</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>
|
||||
@ -202,19 +207,11 @@
|
||||
<h4 class="text-u-c"><i class="fa fa-tag m-r-xs"></i> {{ 'app.admin.members_edit.next_events' | translate }}</h4>
|
||||
</div>
|
||||
<div class="widget-content bg-light wrapper r-b">
|
||||
<ul class="list-unstyled" ng-if="user.events_reservations.length > 0">
|
||||
<li ng-repeat="r in user.events_reservations | eventsReservationsFilter:'future'" class="m-b">
|
||||
<a class="font-sbold" ui-sref="app.public.events_show({id: r.reservable.id})">{{r.reservable.title}}</a> - <span class="label label-warning wrapper-sm">{{ r.start_at | amDateFormat:'LLL' }} - {{ r.end_at | amDateFormat:'LT' }}</span>
|
||||
<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>
|
||||
<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>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="list-unstyled" ng-if="user.events_reservations.length > 0">
|
||||
<div ng-repeat="r in user.events_reservations | eventsReservationsFilter:'future'" class="m-b">
|
||||
<event-reservation-item reservation="r"></event-reservation-item>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="(user.events_reservations | eventsReservationsFilter:'future').length == 0" translate>{{ 'app.admin.members_edit.no_upcoming_events' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -225,11 +222,11 @@
|
||||
<h4 class="text-u-c"><i class="fa fa-tag m-r-xs"></i> {{ 'app.admin.members_edit.passed_events' | translate }}</h4>
|
||||
</div>
|
||||
<div class="widget-content bg-light auto wrapper r-b">
|
||||
<ul class="list-unstyled" ng-if="user.events_reservations.length > 0">
|
||||
<li ng-repeat="r in user.events_reservations | eventsReservationsFilter:'passed'" class="m-b">
|
||||
<span class="font-sbold">{{r.reservable.title}}</span> - <span class="label label-info text-white wrapper-sm">{{ r.start_at | amDateFormat:'LLL' }} - {{ r.end_at | amDateFormat:'LT' }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="list-unstyled" ng-if="user.events_reservations.length > 0">
|
||||
<div ng-repeat="r in user.events_reservations | eventsReservationsFilter:'passed'" class="m-b">
|
||||
<event-reservation-item reservation="r"></event-reservation-item>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="(user.events_reservations | eventsReservationsFilter:'passed').length == 0" translate>{{ 'app.admin.members_edit.no_passed_events' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<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>
|
||||
|
||||
<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,7 @@
|
||||
<user-validation-setting on-success="onSuccess" on-error="onError" />
|
||||
</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>
|
||||
@ -73,6 +74,37 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel panel-default m-t-md">
|
||||
<div class="panel-heading">
|
||||
<span class="font-sbold" translate>{{ 'app.admin.settings.family_account' }}</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="row">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="panel panel-default m-t-md">
|
||||
<div class="panel-heading">
|
||||
<span class="font-sbold" translate>{{ 'app.admin.settings.accounts_management' }}</span>
|
||||
@ -156,4 +188,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>
|
@ -8,7 +8,7 @@
|
||||
</section>
|
||||
|
||||
|
||||
<div class="row no-gutter">
|
||||
<div class="row events-dashboard">
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="widget panel b-a m m-t-lg">
|
||||
@ -16,25 +16,11 @@
|
||||
<h4 class="text-u-c"><i class="fa fa-tag m-r-xs"></i> {{ 'app.logged.dashboard.events.your_next_events' | translate }}</h4>
|
||||
</div>
|
||||
<div class="widget-content bg-light wrapper r-b">
|
||||
<ul class="list-unstyled" ng-if="user.events_reservations.length > 0">
|
||||
<li ng-repeat="r in user.events_reservations | eventsReservationsFilter:'future'" class="m-b">
|
||||
<a class="font-sbold" ui-sref="app.public.events_show({id: r.reservable.id})">{{r.reservable.title}}</a>
|
||||
-
|
||||
<span class="label label-warning wrapper-sm">{{ r.start_at | amDateFormat:'LLL' }} - {{ r.end_at | amDateFormat:'LT' }}</span>
|
||||
<br/>
|
||||
<span translate
|
||||
translate-values="{NUMBER: r.nb_reserve_places}">
|
||||
{{ 'app.logged.dashboard.events.NUMBER_normal_places_reserved' }}
|
||||
</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>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="list-unstyled event-reservation" ng-if="user.events_reservations.length > 0">
|
||||
<div ng-repeat="r in user.events_reservations | eventsReservationsFilter:'future'">
|
||||
<event-reservation-item reservation="r"></event-reservation-item>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="(user.events_reservations | eventsReservationsFilter:'future').length == 0" translate>{{ 'app.logged.dashboard.events.no_events_to_come' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -45,11 +31,11 @@
|
||||
<h4 class="text-u-c"><i class="fa fa-tag m-r-xs"></i> {{ 'app.logged.dashboard.events.your_previous_events' | translate }}</h4>
|
||||
</div>
|
||||
<div class="widget-content bg-light auto wrapper r-b">
|
||||
<ul class="list-unstyled" ng-if="user.events_reservations.length > 0">
|
||||
<li ng-repeat="r in user.events_reservations | eventsReservationsFilter:'passed'" class="m-b">
|
||||
<span class="font-sbold">{{r.reservable.title}}</span> - <span class="label label-info text-white wrapper-sm">{{ r.start_at | amDateFormat:'LLL' }} - {{ r.end_at | amDateFormat:'LT' }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="list-unstyled" ng-if="user.events_reservations.length > 0">
|
||||
<div ng-repeat="r in user.events_reservations | eventsReservationsFilter:'passed'" class="m-b">
|
||||
<event-reservation-item reservation="r"></event-reservation-item>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="(user.events_reservations | eventsReservationsFilter:'passed').length == 0" translate>{{ 'app.logged.dashboard.events.no_passed_events' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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,12 @@
|
||||
</div>
|
||||
|
||||
<div class="panel-content wrapper">
|
||||
<div>
|
||||
<span ng-if="event.event_type === 'nominative'" class="v-middle badge text-xs bg-event" translate="">{{ 'app.public.events_show.event_type.nominative' }}</span>
|
||||
<span ng-if="event.event_type === 'family'" class="v-middle badge text-xs bg-event" translate="">{{ 'app.public.events_show.event_type.family' }}</span>
|
||||
<span ng-if="event.pre_registration" class="v-middle badge text-xs bg-info" translate="">{{ 'app.public.events_show.pre_registration' }}</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}}
|
||||
@ -86,6 +90,8 @@
|
||||
<dt><i class="fas fa-clock"></i> {{ 'app.public.events_show.opening_hours' | translate }}</dt>
|
||||
<dd ng-if="event.all_day"><span translate>{{ 'app.public.events_show.all_day' }}</span></dd>
|
||||
<dd ng-if="!event.all_day">{{ 'app.public.events_show.from_time' | translate }} <span class="text-u-l">{{event.start_time}}</span> {{ 'app.public.events_show.to_time' | translate }} <span class="text-u-l">{{event.end_time}}</span></dd>
|
||||
<dt ng-if="event.pre_registration_end_date"><i class="fa fa-calendar" aria-hidden="true"></i> {{ 'app.public.events_show.pre_registration_end_date' | translate }}</dt>
|
||||
<dd ng-if="event.pre_registration_end_date"><span class="text-u-l">{{event.pre_registration_end_date | amDateFormat:'L'}}</span></dd>
|
||||
</dl>
|
||||
|
||||
<div class="text-sm" ng-if="event.amount">
|
||||
@ -116,19 +122,81 @@
|
||||
<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.validated_at" 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.validated_at">
|
||||
<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">
|
||||
<div ng-show="currentUser.role == 'admin' && !event.pre_registration" class="m-t">
|
||||
<label for="offerSlot" class="control-label m-r" translate>{{ 'app.public.events_show.make_a_gift_of_this_reservation' }}</label>
|
||||
<input bs-switch
|
||||
ng-model="event.offered"
|
||||
@ -150,14 +218,18 @@
|
||||
<a class="pull-right m-t-xs text-u-l ng-scope" ng-click="cancelReserve($event)" ng-show="reserve.toReserve" translate>{{ 'app.shared.buttons.cancel' }}</a>
|
||||
</div>
|
||||
|
||||
<div ng-if="reserveSuccess" class="alert alert-success">{{ 'app.public.events_show.thank_you_your_payment_has_been_successfully_registered' | translate }}<br>
|
||||
{{ 'app.public.events_show.you_can_find_your_reservation_s_details_on_your_' | translate }} <a ui-sref="app.logged.dashboard.invoices" translate>{{ 'app.public.events_show.dashboard' }}</a>
|
||||
<div ng-if="reserveSuccess && !event.pre_registration" class="alert alert-success">{{ 'app.public.events_show.thank_you_your_payment_has_been_successfully_registered' | translate }}<br>
|
||||
{{ 'app.public.events_show.you_can_find_your_reservation_s_details_on_your_' | translate }} <a ui-sref="app.logged.dashboard.events" translate>{{ 'app.public.events_show.dashboard' }}</a>
|
||||
</div>
|
||||
<div ng-if="reserveSuccess && event.pre_registration" class="alert alert-success">{{ 'app.public.events_show.thank_you_your_pre_registration_has_been_successfully_saved' | translate }}<br>
|
||||
{{ 'app.public.events_show.informed_by_email_your_pre_registration' | translate }}
|
||||
</div>
|
||||
<div class="m-t-sm" ng-if="reservations && !reserve.toReserve" ng-repeat="reservation in reservations">
|
||||
<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-u-c text-sm" ng-if="!event.pre_registration">{{ 'app.public.events_show.you_booked_DATE' | translate:{DATE:(reservation.created_at | amDateFormat:'L LT')} }}</div>
|
||||
<div class="font-sbold text-u-c text-sm" ng-if="event.pre_registration">{{ 'app.public.events_show.you_pre_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)">
|
||||
@ -178,7 +250,10 @@
|
||||
<span ng-show="reservations.length > 0" translate>{{ 'app.public.events_show.thanks_for_coming' }}</span>
|
||||
<a ui-sref="app.public.events_list" translate>{{ 'app.public.events_show.view_event_list' }}</a>
|
||||
</div>
|
||||
<button class="btn btn-warning-full rounded btn-block text-sm" ng-click="reserveEvent()" ng-show="isShowReserveEventButton()">{{ 'app.public.events_show.book' | translate }}</button>
|
||||
<button class="btn btn-warning-full rounded btn-block text-sm" ng-click="reserveEvent()" ng-show="isShowReserveEventButton()">
|
||||
<span ng-if="event.pre_registration">{{ 'app.public.events_show.pre_book' | translate }}</span>
|
||||
<span ng-if="!event.pre_registration">{{ 'app.public.events_show.book' | translate }}</span>
|
||||
</button>
|
||||
<uib-alert type="danger" ng-if="ctrl.member.id && !isUserValidatedByType()">
|
||||
<p class="text-sm">
|
||||
<i class="fa fa-warning"></i>
|
||||
@ -186,15 +261,15 @@
|
||||
</p>
|
||||
</uib-alert>
|
||||
|
||||
<coupon show="reserve.totalSeats > 0 && ctrl.member" coupon="coupon.applied" total="reserve.totalNoCoupon" user-id="{{ctrl.member.id}}"></coupon>
|
||||
<coupon show="reserve.totalSeats > 0 && ctrl.member && !event.pre_registration" coupon="coupon.applied" total="reserve.totalNoCoupon" user-id="{{ctrl.member.id}}"></coupon>
|
||||
</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.pre_registration && 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.pre_registration || 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">
|
||||
<span translate>{{ 'app.shared.member_select.member_not_validated' }}</span>
|
||||
</div>
|
||||
<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>
|
||||
</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>
|
||||
|
@ -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
|
@ -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
|
||||
@ -85,14 +87,21 @@ class Event < ApplicationRecord
|
||||
if nb_total_places.nil?
|
||||
self.nb_free_places = nil
|
||||
else
|
||||
reserved_places = reservations.joins(:slots_reservations)
|
||||
.where('slots_reservations.canceled_at': nil)
|
||||
.map(&:total_booked_seats)
|
||||
.inject(0) { |sum, t| sum + t }
|
||||
reserved = reservations.joins(:slots_reservations).where('slots_reservations.canceled_at': nil)
|
||||
reserved = reserved.where('slots_reservations.is_valid': true) if pre_registration?
|
||||
reserved_places = reserved.map(&:total_booked_seats).inject(0) { |sum, t| sum + t }
|
||||
self.nb_free_places = (nb_total_places - reserved_places)
|
||||
end
|
||||
end
|
||||
|
||||
def nb_places_for_pre_registration
|
||||
reservations.joins(:slots_reservations)
|
||||
.where('slots_reservations.canceled_at': nil)
|
||||
.where('slots_reservations.is_valid': nil)
|
||||
.map(&:total_booked_seats)
|
||||
.inject(0) { |sum, t| sum + t }
|
||||
end
|
||||
|
||||
def all_day?
|
||||
availability.start_at.hour.zero?
|
||||
end
|
||||
|
@ -24,6 +24,8 @@ class Reservation < ApplicationRecord
|
||||
has_many :prepaid_pack_reservations, dependent: :destroy
|
||||
|
||||
belongs_to :reservation_context
|
||||
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) }
|
||||
@ -130,15 +132,27 @@ class Reservation < ApplicationRecord
|
||||
end
|
||||
|
||||
def notify_member_create_reservation
|
||||
NotificationCenter.call type: 'notify_member_create_reservation',
|
||||
receiver: user,
|
||||
attached_object: self
|
||||
if reservable_type == 'Event' && reservable.pre_registration?
|
||||
NotificationCenter.call type: 'notify_member_pre_booked_reservation',
|
||||
receiver: user,
|
||||
attached_object: self
|
||||
else
|
||||
NotificationCenter.call type: 'notify_member_create_reservation',
|
||||
receiver: user,
|
||||
attached_object: self
|
||||
end
|
||||
end
|
||||
|
||||
def notify_admin_member_create_reservation
|
||||
NotificationCenter.call type: 'notify_admin_member_create_reservation',
|
||||
receiver: User.admins_and_managers,
|
||||
attached_object: self
|
||||
if reservable_type == 'Event' && reservable.pre_registration?
|
||||
NotificationCenter.call type: 'notify_admin_member_pre_booked_reservation',
|
||||
receiver: User.admins_and_managers,
|
||||
attached_object: self
|
||||
else
|
||||
NotificationCenter.call type: 'notify_admin_member_create_reservation',
|
||||
receiver: User.admins_and_managers,
|
||||
attached_object: self
|
||||
end
|
||||
end
|
||||
|
||||
def notify_member_limitation_reached
|
||||
|
@ -60,15 +60,26 @@ class ShoppingCart
|
||||
items.each do |item|
|
||||
objects.push(save_item(item))
|
||||
end
|
||||
update_credits(objects)
|
||||
update_packs(objects)
|
||||
event_reservation = objects.find { |o| o.is_a?(Reservation) && o.reservable_type == 'Event' }
|
||||
if event_reservation&.reservable&.pre_registration
|
||||
payment = Invoice.new(
|
||||
invoicing_profile: @customer.invoicing_profile,
|
||||
statistic_profile: @customer.statistic_profile,
|
||||
operator_profile_id: @operator.invoicing_profile.id,
|
||||
payment_method: @payment_method,
|
||||
total: 0
|
||||
)
|
||||
else
|
||||
update_credits(objects)
|
||||
update_packs(objects)
|
||||
|
||||
payment = create_payment_document(price, objects, payment_id, payment_type)
|
||||
WalletService.debit_user_wallet(payment, @customer)
|
||||
next if Setting.get('prevent_invoices_zero') && price[:total].zero?
|
||||
payment = create_payment_document(price, objects, payment_id, payment_type)
|
||||
WalletService.debit_user_wallet(payment, @customer)
|
||||
next if Setting.get('prevent_invoices_zero') && price[:total].zero?
|
||||
|
||||
payment.save
|
||||
payment.post_save(payment_id, payment_type)
|
||||
payment.save
|
||||
payment.post_save(payment_id, payment_type)
|
||||
end
|
||||
end
|
||||
|
||||
success = !payment.nil? && objects.map(&:errors).flatten.map(&:empty?).all? && items.map(&:errors).map(&:blank?).all?
|
||||
|
@ -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,8 +2,9 @@
|
||||
|
||||
# An admin can mark an uploaded document as refused, this will notify the member
|
||||
class SupportingDocumentRefusal < ApplicationRecord
|
||||
belongs_to :user
|
||||
include NotificationAttachedObject
|
||||
|
||||
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
|
||||
has_and_belongs_to_many :supporting_document_types
|
||||
end
|
||||
|
@ -6,6 +6,7 @@ class SupportingDocumentType < ApplicationRecord
|
||||
|
||||
has_many :supporting_document_files, dependent: :destroy
|
||||
|
||||
has_many :supporting_document_refusals_types, dependent: :destroy
|
||||
has_many :supporting_document_refusals, through: :supporting_document_refusals_types
|
||||
has_and_belongs_to_many :supporting_document_refusals, dependent: :destroy
|
||||
|
||||
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
|
@ -5,7 +5,8 @@ class LocalPaymentPolicy < ApplicationPolicy
|
||||
def confirm_payment?
|
||||
# only admins and managers can offer free extensions of a subscription
|
||||
has_free_days = record.shopping_cart.items.any? { |item| item.is_a? CartItem::FreeExtension }
|
||||
event = record.shopping_cart.items.find { |item| item.is_a? CartItem::EventReservation }
|
||||
|
||||
((user.admin? || user.manager?) && record.shopping_cart.customer.id != user.id) || (record.price.zero? && !has_free_days)
|
||||
((user.admin? || user.manager?) && record.shopping_cart.customer.id != user.id) || (record.price.zero? && !has_free_days) || event&.reservable&.pre_registration
|
||||
end
|
||||
end
|
||||
|
@ -9,4 +9,8 @@ class ReservationPolicy < ApplicationPolicy
|
||||
def update?
|
||||
user.admin? || user.manager? || record.user == user
|
||||
end
|
||||
|
||||
def confirm_payment?
|
||||
user.admin? || user.manager?
|
||||
end
|
||||
end
|
||||
|
@ -47,7 +47,7 @@ class SettingPolicy < ApplicationPolicy
|
||||
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 projects_list_member_filter_presence projects_list_date_filters_presence advanced_accounting
|
||||
project_categories_filter_placeholder project_categories_wording reservation_context_feature]
|
||||
project_categories_filter_placeholder project_categories_wording reservation_context_feature family_account child_validation_required]
|
||||
end
|
||||
|
||||
##
|
||||
|
@ -15,4 +15,12 @@ class SlotsReservationPolicy < ApplicationPolicy
|
||||
def cancel?
|
||||
user.admin? || user.manager? || record.reservation.user == user
|
||||
end
|
||||
|
||||
def validate?
|
||||
user.admin? || user.manager?
|
||||
end
|
||||
|
||||
def invalidate?
|
||||
user.admin? || user.manager?
|
||||
end
|
||||
end
|
||||
|
@ -6,15 +6,11 @@ class SupportingDocumentFilePolicy < ApplicationPolicy
|
||||
user.privileged?
|
||||
end
|
||||
|
||||
def create?
|
||||
user.privileged? or record.user_id == user.id
|
||||
end
|
||||
|
||||
def update?
|
||||
user.privileged? or record.user_id == user.id
|
||||
end
|
||||
|
||||
def download?
|
||||
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
|
||||
end
|
||||
end
|
||||
|
@ -173,7 +173,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,
|
||||
|
61
app/services/child_service.rb
Normal file
61
app/services/child_service.rb
Normal file
@ -0,0 +1,61 @@
|
||||
# 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)
|
||||
if file.nil? || file.attachment_identifier.nil? || child_params['supporting_document_files_attributes']['0']['attachment'].blank?
|
||||
all_files_are_upload = false
|
||||
end
|
||||
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
|
@ -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
|
||||
|
86
app/services/reservation_confirm_payment_service.rb
Normal file
86
app/services/reservation_confirm_payment_service.rb
Normal file
@ -0,0 +1,86 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# confirm payment of a pre-registration reservation
|
||||
class ReservationConfirmPaymentService
|
||||
def initialize(reservation, operator, coupon, offered)
|
||||
@reservation = reservation
|
||||
@operator = operator
|
||||
@offered = offered
|
||||
@coupon = CartItem::Coupon.new(
|
||||
customer_profile: @reservation.user.invoicing_profile,
|
||||
operator_profile: @operator.invoicing_profile,
|
||||
coupon: Coupon.find_by(code: coupon)
|
||||
)
|
||||
end
|
||||
|
||||
def total
|
||||
slots_reservations = @reservation.slots_reservations.map do |sr|
|
||||
{
|
||||
slot_id: sr.slot_id,
|
||||
offered: @offered
|
||||
}
|
||||
end
|
||||
tickets = @reservation.tickets.map do |t|
|
||||
{
|
||||
event_price_category_id: t.event_price_category_id,
|
||||
booked: t.booked
|
||||
}
|
||||
end
|
||||
booking_users = @reservation.booking_users.map do |bu|
|
||||
{
|
||||
name: bu.name,
|
||||
event_price_category_id: bu.event_price_category_id,
|
||||
booked_id: bu.booked_id,
|
||||
booked_type: bu.booked_type
|
||||
}
|
||||
end
|
||||
event_reservation = CartItem::EventReservation.new(customer_profile: @reservation.user.invoicing_profile,
|
||||
operator_profile: @operator.invoicing_profile,
|
||||
event: @reservation.reservable,
|
||||
cart_item_reservation_slots_attributes: slots_reservations,
|
||||
normal_tickets: @reservation.nb_reserve_places,
|
||||
cart_item_event_reservation_tickets_attributes: tickets,
|
||||
cart_item_event_reservation_booking_users_attributes: booking_users)
|
||||
|
||||
all_elements = {
|
||||
slots: @reservation.slots_reservations.map do |sr|
|
||||
{ start_at: sr.slot.start_at, end_at: sr.slot.end_at, price: event_reservation.price[:amount] }
|
||||
end
|
||||
}
|
||||
|
||||
total_amount = event_reservation.price[:amount]
|
||||
|
||||
coupon_info = @coupon.price(total_amount)
|
||||
|
||||
# return result
|
||||
{
|
||||
elements: all_elements,
|
||||
total: coupon_info[:total_with_coupon].to_i,
|
||||
before_coupon: coupon_info[:total_without_coupon].to_i,
|
||||
coupon: @coupon.coupon
|
||||
}
|
||||
end
|
||||
|
||||
def call
|
||||
price = total
|
||||
invoice = InvoicesService.create(
|
||||
price,
|
||||
@operator.invoicing_profile.id,
|
||||
[@reservation],
|
||||
@reservation.user
|
||||
)
|
||||
if Setting.get('prevent_invoices_zero') && price[:total].zero?
|
||||
@reservation.slots_reservations.first.update(is_confirm: true)
|
||||
return invoice
|
||||
end
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
WalletService.debit_user_wallet(invoice, @reservation.user)
|
||||
|
||||
invoice.save
|
||||
invoice.post_save
|
||||
@reservation.slots_reservations.first.update(is_confirm: true)
|
||||
end
|
||||
invoice
|
||||
end
|
||||
end
|
@ -19,6 +19,9 @@ class Slots::ReservationsService
|
||||
.where('reservations.reservable_type': reservable_types)
|
||||
.where('reservations.reservable_id': reservables.map { |r| r.try(:id) })
|
||||
.where('slots_reservations.canceled_at': nil)
|
||||
if reservables.first.is_a?(Event) && reservables.first&.pre_registration?
|
||||
reservations = reservations.where('slots_reservations.is_valid': true)
|
||||
end
|
||||
|
||||
user_ids = reservations.includes(reservation: :statistic_profile)
|
||||
.map(&:reservation)
|
||||
|
@ -21,5 +21,43 @@ class SlotsReservationsService
|
||||
rescue Faraday::ConnectionFailed
|
||||
warn 'Unable to update data in elasticsearch'
|
||||
end
|
||||
|
||||
def validate(slot_reservation)
|
||||
if slot_reservation.update(is_valid: true)
|
||||
reservable = slot_reservation.reservation.reservable
|
||||
if reservable.is_a?(Event)
|
||||
reservable.update_nb_free_places
|
||||
reservable.save
|
||||
end
|
||||
Slots::PlacesCacheService.refresh(slot_reservation.slot)
|
||||
NotificationCenter.call type: 'notify_member_reservation_validated',
|
||||
receiver: slot_reservation.reservation.user,
|
||||
attached_object: slot_reservation.reservation
|
||||
NotificationCenter.call type: 'notify_admin_reservation_validated',
|
||||
receiver: User.admins_and_managers,
|
||||
attached_object: slot_reservation.reservation
|
||||
return true
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
def invalidate(slot_reservation)
|
||||
if slot_reservation.update(is_valid: false)
|
||||
reservable = slot_reservation.reservation.reservable
|
||||
if reservable.is_a?(Event)
|
||||
reservable.update_nb_free_places
|
||||
reservable.save
|
||||
end
|
||||
Slots::PlacesCacheService.refresh(slot_reservation.slot)
|
||||
NotificationCenter.call type: 'notify_member_reservation_invalidated',
|
||||
receiver: slot_reservation.reservation.user,
|
||||
attached_object: slot_reservation.reservation
|
||||
NotificationCenter.call type: 'notify_admin_reservation_invalidated',
|
||||
receiver: User.admins_and_managers,
|
||||
attached_object: slot_reservation.reservation
|
||||
return true
|
||||
end
|
||||
false
|
||||
end
|
||||
end
|
||||
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
|
||||
user.group.supporting_document_types.each do |type|
|
||||
file = type.supporting_document_files.find_by(user_id: supporting_document_file.user_id)
|
||||
all_files_are_upload = false unless file
|
||||
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(supportable_id: supporting_document_file.supportable_id,
|
||||
supportable_type: supporting_document_file.supportable_type)
|
||||
all_files_are_upload = false unless file
|
||||
end
|
||||
end
|
||||
if all_files_are_upload
|
||||
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
|
||||
user.group.supporting_document_types.each do |type|
|
||||
file = type.supporting_document_files.find_by(user_id: supporting_document_file.user_id)
|
||||
all_files_are_upload = false unless file
|
||||
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(supportable_id: supporting_document_file.supportable_id,
|
||||
supportable_type: supporting_document_file.supportable_type)
|
||||
all_files_are_upload = false unless file
|
||||
end
|
||||
end
|
||||
if all_files_are_upload
|
||||
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
|
||||
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,
|
||||
attached_object: supporting_document_refusal
|
||||
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.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…
Reference in New Issue
Block a user