mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-02-20 14:54:15 +01:00
(ui) refactor event form
This commit is contained in:
parent
8df60a8712
commit
3d796549f2
10
app/frontend/src/javascript/api/age-range.ts
Normal file
10
app/frontend/src/javascript/api/age-range.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import apiClient from './clients/api-client';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { AgeRange } from '../models/event';
|
||||
|
||||
export default class AgeRangeAPI {
|
||||
static async index (): Promise<Array<AgeRange>> {
|
||||
const res: AxiosResponse<Array<AgeRange>> = await apiClient.get('/api/age_ranges');
|
||||
return res?.data;
|
||||
}
|
||||
}
|
10
app/frontend/src/javascript/api/event-category.ts
Normal file
10
app/frontend/src/javascript/api/event-category.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import apiClient from './clients/api-client';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { EventCategory } from '../models/event';
|
||||
|
||||
export default class EventCategoryAPI {
|
||||
static async index (): Promise<Array<EventCategory>> {
|
||||
const res: AxiosResponse<Array<EventCategory>> = await apiClient.get('/api/categories');
|
||||
return res?.data;
|
||||
}
|
||||
}
|
10
app/frontend/src/javascript/api/event-price-category.ts
Normal file
10
app/frontend/src/javascript/api/event-price-category.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import apiClient from './clients/api-client';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { EventPriceCategory } from '../models/event';
|
||||
|
||||
export default class EventPriceCategoryAPI {
|
||||
static async index (): Promise<Array<EventPriceCategory>> {
|
||||
const res: AxiosResponse<Array<EventPriceCategory>> = await apiClient.get('/api/price_categories');
|
||||
return res?.data;
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import apiClient from './clients/api-client';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { EventTheme } from '../models/event-theme';
|
||||
import { EventTheme } from '../models/event';
|
||||
|
||||
export default class EventThemeAPI {
|
||||
static async index (): Promise<Array<EventTheme>> {
|
||||
|
27
app/frontend/src/javascript/api/event.ts
Normal file
27
app/frontend/src/javascript/api/event.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import apiClient from './clients/api-client';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { Event, EventUpdateResult } from '../models/event';
|
||||
import ApiLib from '../lib/api';
|
||||
|
||||
export default class EventAPI {
|
||||
static async create (event: Event): Promise<Event> {
|
||||
const data = ApiLib.serializeAttachments(event, 'event', ['event_files_attributes', 'event_image_attributes']);
|
||||
const res: AxiosResponse<Event> = await apiClient.post('/api/events', data, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async update (event: Event, mode: 'single' | 'next' | 'all'): Promise<EventUpdateResult> {
|
||||
const data = ApiLib.serializeAttachments(event, 'event', ['event_files_attributes', 'event_image_attributes']);
|
||||
data.set('edit_mode', mode);
|
||||
const res: AxiosResponse<EventUpdateResult> = await apiClient.put(`/api/events/${event.id}`, data, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
return res?.data;
|
||||
}
|
||||
}
|
@ -53,14 +53,14 @@ export const EventCard: React.FC<EventCardProps> = ({ event, cardType }) => {
|
||||
const formatTime = (): string => {
|
||||
return event.all_day
|
||||
? t('app.public.event_card.all_day')
|
||||
: t('app.public.event_card.from_time_to_time', { START: FormatLib.time(event.start_date), END: FormatLib.time(event.end_date) });
|
||||
: t('app.public.event_card.from_time_to_time', { START: FormatLib.time(event.start_time), END: FormatLib.time(event.end_time) });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`event-card event-card--${cardType}`}>
|
||||
{event.event_image
|
||||
{event.event_image_attributes
|
||||
? <div className="event-card-picture">
|
||||
<img src={event.event_image} alt="" />
|
||||
<img src={event.event_image_attributes.attachment_url} alt={event.event_image_attributes.attachment_name} />
|
||||
</div>
|
||||
: cardType !== 'sm' &&
|
||||
<div className="event-card-picture">
|
||||
|
315
app/frontend/src/javascript/components/events/event-form.tsx
Normal file
315
app/frontend/src/javascript/components/events/event-form.tsx
Normal file
@ -0,0 +1,315 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { SubmitHandler, useFieldArray, useForm, useWatch } from 'react-hook-form';
|
||||
import { Event, EventDecoration, EventPriceCategoryAttributes, RecurrenceOption } from '../../models/event';
|
||||
import EventAPI from '../../api/event';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FormInput } from '../form/form-input';
|
||||
import { FormImageUpload } from '../form/form-image-upload';
|
||||
import { IApplication } from '../../models/application';
|
||||
import { Loader } from '../base/loader';
|
||||
import { react2angular } from 'react2angular';
|
||||
import { ErrorBoundary } from '../base/error-boundary';
|
||||
import { FormRichText } from '../form/form-rich-text';
|
||||
import { FormMultiFileUpload } from '../form/form-multi-file-upload';
|
||||
import { FabButton } from '../base/fab-button';
|
||||
import { FormSwitch } from '../form/form-switch';
|
||||
import { SelectOption } from '../../models/select';
|
||||
import EventCategoryAPI from '../../api/event-category';
|
||||
import { FormSelect } from '../form/form-select';
|
||||
import EventThemeAPI from '../../api/event-theme';
|
||||
import { FormMultiSelect } from '../form/form-multi-select';
|
||||
import AgeRangeAPI from '../../api/age-range';
|
||||
import { Plus, Trash } from 'phosphor-react';
|
||||
import FormatLib from '../../lib/format';
|
||||
import EventPriceCategoryAPI from '../../api/event-price-category';
|
||||
import { UpdateRecurrentModal } from './update-recurrent-modal';
|
||||
|
||||
declare const Application: IApplication;
|
||||
|
||||
interface EventFormProps {
|
||||
action: 'create' | 'update',
|
||||
event?: Event,
|
||||
onError: (message: string) => void,
|
||||
onSuccess: (message: string) => void,
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 output = useWatch<Event>({ control });
|
||||
const { fields, append, remove } = useFieldArray({ control, name: 'event_price_categories_attributes' });
|
||||
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
const [isAllDay, setIsAllDay] = useState<boolean>(event?.all_day);
|
||||
const [categoriesOptions, setCategoriesOptions] = useState<Array<SelectOption<number>>>([]);
|
||||
const [themesOptions, setThemesOptions] = useState<Array<SelectOption<number>>>(null);
|
||||
const [ageRangeOptions, setAgeRangeOptions] = useState<Array<SelectOption<number>>>(null);
|
||||
const [priceCategoriesOptions, setPriceCategoriesOptions] = useState<Array<SelectOption<number>>>(null);
|
||||
const [isOpenRecurrentModal, setIsOpenRecurrentModal] = useState<boolean>(false);
|
||||
const [updatingEvent, setUpdatingEvent] = useState<Event>(null);
|
||||
|
||||
useEffect(() => {
|
||||
EventCategoryAPI.index()
|
||||
.then(data => setCategoriesOptions(data.map(m => decorationToOption(m))))
|
||||
.catch(onError);
|
||||
EventThemeAPI.index()
|
||||
.then(data => setThemesOptions(data.map(t => decorationToOption(t))))
|
||||
.catch(onError);
|
||||
AgeRangeAPI.index()
|
||||
.then(data => setAgeRangeOptions(data.map(r => decorationToOption(r))))
|
||||
.catch(onError);
|
||||
EventPriceCategoryAPI.index()
|
||||
.then(data => setPriceCategoriesOptions(data.map(c => decorationToOption(c))))
|
||||
.catch(onError);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Callback triggered when the user clicks on the 'remove' button, in the additional prices area
|
||||
*/
|
||||
const handlePriceRemove = (price: EventPriceCategoryAttributes, index: number) => {
|
||||
if (!price.id) return remove(index);
|
||||
|
||||
setValue(`event_price_categories_attributes.${index}._destroy`, true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when the user validates the machine form: handle create or update
|
||||
*/
|
||||
const onSubmit: SubmitHandler<Event> = (data: Event) => {
|
||||
if (action === 'update') {
|
||||
if (event?.recurrence_events?.length > 0) {
|
||||
setUpdatingEvent(data);
|
||||
toggleRecurrentModal();
|
||||
} else {
|
||||
handleUpdateRecurrentConfirmed(data, 'single');
|
||||
}
|
||||
} else {
|
||||
EventAPI.create(data).then(res => {
|
||||
onSuccess(t(`app.admin.event_form.${action}_success`));
|
||||
window.location.href = `/#!/events/${res.id}`;
|
||||
}).catch(onError);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Open/closes the confirmation modal for updating recurring events
|
||||
*/
|
||||
const toggleRecurrentModal = () => {
|
||||
setIsOpenRecurrentModal(!isOpenRecurrentModal);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if any dates have changed
|
||||
*/
|
||||
const datesHaveChanged = (): boolean => {
|
||||
return ((event?.start_date !== (updatingEvent?.start_date as Date)?.toISOString()?.substring(0, 10)) ||
|
||||
(event?.end_date !== (updatingEvent?.end_date as Date)?.toISOString()?.substring(0, 10)));
|
||||
};
|
||||
|
||||
/**
|
||||
* When the user has confirmed the update of the other occurences (or not), proceed with the API update
|
||||
* and handle the result
|
||||
*/
|
||||
const handleUpdateRecurrentConfirmed = (data: Event, mode: 'single' | 'next' | 'all') => {
|
||||
EventAPI.update(data, mode).then(res => {
|
||||
if (res.total === res.updated) {
|
||||
onSuccess(t('app.admin.event_form.events_updated', { COUNT: res.updated }));
|
||||
} else {
|
||||
onError(t('app.admin.event_form.events_not_updated', { TOTAL: res.total, COUNT: res.total - res.updated }));
|
||||
if (res.details.events.find(d => d.error === 'EventPriceCategory')) {
|
||||
onError(t('app.admin.event_form.error_deleting_reserved_price'));
|
||||
} else {
|
||||
onError(t('app.admin.event_form.other_error'));
|
||||
}
|
||||
}
|
||||
window.location.href = '/#!/events';
|
||||
}).catch(onError);
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert an event-decoration (category/theme/etc.) to an option usable by react-select
|
||||
*/
|
||||
const decorationToOption = (item: EventDecoration): SelectOption<number> => {
|
||||
return { value: item.id, label: item.name };
|
||||
};
|
||||
|
||||
/**
|
||||
* In 'create' mode, the user can choose if the new event will be recurrent.
|
||||
* This method provides teh various options for recurrence
|
||||
*/
|
||||
const buildRecurrenceOptions = (): Array<SelectOption<RecurrenceOption>> => {
|
||||
return [
|
||||
{ label: t('app.admin.event_form.recurring.none'), value: 'none' },
|
||||
{ label: t('app.admin.event_form.recurring.every_days'), value: 'day' },
|
||||
{ label: t('app.admin.event_form.recurring.every_week'), value: 'week' },
|
||||
{ label: t('app.admin.event_form.recurring.every_month'), value: 'month' },
|
||||
{ label: t('app.admin.event_form.recurring.every_year'), value: 'year' }
|
||||
];
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="event-form" onSubmit={handleSubmit(onSubmit)}>
|
||||
<FormInput register={register}
|
||||
id="title"
|
||||
formState={formState}
|
||||
rules={{ required: true }}
|
||||
label={t('app.admin.event_form.title')} />
|
||||
<FormImageUpload setValue={setValue}
|
||||
register={register}
|
||||
control={control}
|
||||
formState={formState}
|
||||
rules={{ required: true }}
|
||||
id="event_image_attributes"
|
||||
accept="image/*"
|
||||
defaultImage={output.event_image_attributes}
|
||||
label={t('app.admin.event_form.matching_visual')} />
|
||||
<FormRichText control={control}
|
||||
id="description"
|
||||
rules={{ required: true }}
|
||||
label={t('app.admin.event_form.description')}
|
||||
limit={null}
|
||||
heading bulletList blockquote link video image />
|
||||
<FormSelect id="category_id"
|
||||
control={control}
|
||||
formState={formState}
|
||||
label={t('app.admin.event_form.event_category')}
|
||||
options={categoriesOptions}
|
||||
rules={{ required: true }} />
|
||||
{themesOptions && <FormMultiSelect control={control}
|
||||
id="event_theme_ids"
|
||||
formState={formState}
|
||||
options={themesOptions}
|
||||
label={t('app.admin.event_form.event_themes')} />}
|
||||
{ageRangeOptions && <FormSelect control={control}
|
||||
id="age_range_id"
|
||||
formState={formState}
|
||||
options={ageRangeOptions}
|
||||
label={t('app.admin.event_form.age_range')} />}
|
||||
<div className="dates-times">
|
||||
<h4>{t('app.admin.event_form.dates_and_opening_hours')}</h4>
|
||||
<div className="dates">
|
||||
<FormInput id="start_date"
|
||||
type="date"
|
||||
register={register}
|
||||
formState={formState}
|
||||
label={t('app.admin.event_form.start_date')}
|
||||
rules={{ required: true }} />
|
||||
<FormInput id="end_date"
|
||||
type="date"
|
||||
register={register}
|
||||
formState={formState}
|
||||
label={t('app.admin.event_form.end_date')}
|
||||
rules={{ required: true }} />
|
||||
</div>
|
||||
<FormSwitch control={control}
|
||||
id="all_day"
|
||||
label={t('app.admin.event_form.all_day')}
|
||||
formState={formState}
|
||||
tooltip={t('app.admin.event_form.all_day_help')}
|
||||
onChange={setIsAllDay} />
|
||||
{!isAllDay && <div className="times">
|
||||
<FormInput id="start_time"
|
||||
type="time"
|
||||
register={register}
|
||||
formState={formState}
|
||||
label={t('app.admin.event_form.start_time')}
|
||||
rules={{ required: !isAllDay }} />
|
||||
<FormInput id="end_time"
|
||||
type="time"
|
||||
register={register}
|
||||
formState={formState}
|
||||
label={t('app.admin.event_form.end_time')}
|
||||
rules={{ required: !isAllDay }} />
|
||||
</div> }
|
||||
{action === 'create' && <div className="recurring">
|
||||
<FormSelect options={buildRecurrenceOptions()}
|
||||
control={control}
|
||||
formState={formState}
|
||||
id="recurrence"
|
||||
valueDefault="none"
|
||||
label={t('app.admin.event_form.recurrence')} />
|
||||
<FormInput register={register}
|
||||
id="recurrence_end_at"
|
||||
type="date"
|
||||
formState={formState}
|
||||
nullable
|
||||
defaultValue={null}
|
||||
label={t('app.admin.event_form._and_ends_on')}
|
||||
rules={{ required: !['none', undefined].includes(output.recurrence) }} />
|
||||
</div>}
|
||||
</div>
|
||||
<div className="seats-prices">
|
||||
<h4>{t('app.admin.event_form.prices_and_availabilities')}</h4>
|
||||
<FormInput register={register}
|
||||
id="nb_total_places"
|
||||
label={t('app.admin.event_form.seats_available')}
|
||||
type="number"
|
||||
tooltip={t('app.admin.event_form.seats_help')} />
|
||||
<FormInput register={register}
|
||||
id="amount"
|
||||
formState={formState}
|
||||
rules={{ required: true }}
|
||||
label={t('app.admin.event_form.standard_rate')}
|
||||
tooltip={t('app.admin.event_form.0_equal_free')}
|
||||
addOn={FormatLib.currencySymbol()} />
|
||||
{priceCategoriesOptions && <div className="additional-prices">
|
||||
{fields.map((price, index) => (
|
||||
<div key={index} className={`price-item ${output.event_price_categories_attributes[index]?._destroy ? 'destroyed-item' : ''}`}>
|
||||
<FormSelect options={priceCategoriesOptions}
|
||||
control={control}
|
||||
id={`event_price_categories_attributes.${index}.price_category_id`}
|
||||
rules={{ required: true }}
|
||||
label={t('app.admin.event_form.fare_class')} />
|
||||
<FormInput id={`event_price_categories_attributes.${index}.amount`}
|
||||
register={register}
|
||||
type="number"
|
||||
rules={{ required: true }}
|
||||
label={t('app.admin.event_form.price')}
|
||||
addOn={FormatLib.currencySymbol()} />
|
||||
<FabButton className="remove-price is-main" onClick={() => handlePriceRemove(price, index)} icon={<Trash size={20} />} />
|
||||
</div>
|
||||
))}
|
||||
<FabButton className="add-price is-secondary" onClick={() => append({})}>
|
||||
<Plus size={20} />
|
||||
{t('app.admin.event_form.add_price')}
|
||||
</FabButton>
|
||||
</div>}
|
||||
</div>
|
||||
<div className="attachments">
|
||||
<div className='form-item-header event-files-header'>
|
||||
<h4>{t('app.admin.event_form.attachments')}</h4>
|
||||
</div>
|
||||
<FormMultiFileUpload setValue={setValue}
|
||||
addButtonLabel={t('app.admin.event_form.add_a_new_file')}
|
||||
control={control}
|
||||
accept="application/pdf"
|
||||
register={register}
|
||||
id="event_files_attributes"
|
||||
className="event-files" />
|
||||
</div>
|
||||
<FabButton type="submit" className="is-info submit-btn">
|
||||
{t('app.admin.event_form.ACTION_event', { ACTION: action })}
|
||||
</FabButton>
|
||||
<UpdateRecurrentModal isOpen={isOpenRecurrentModal}
|
||||
toggleModal={toggleRecurrentModal}
|
||||
event={updatingEvent}
|
||||
onConfirmed={handleUpdateRecurrentConfirmed}
|
||||
datesChanged={datesHaveChanged()} />
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
const EventFormWrapper: React.FC<EventFormProps> = (props) => {
|
||||
return (
|
||||
<Loader>
|
||||
<ErrorBoundary>
|
||||
<EventForm {...props} />
|
||||
</ErrorBoundary>
|
||||
</Loader>
|
||||
);
|
||||
};
|
||||
|
||||
Application.Components.component('eventForm', react2angular(EventFormWrapper, ['action', 'event', 'onError', 'onSuccess']));
|
@ -1,96 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Select from 'react-select';
|
||||
import { react2angular } from 'react2angular';
|
||||
import { Loader } from '../base/loader';
|
||||
import { Event } from '../../models/event';
|
||||
import { EventTheme } from '../../models/event-theme';
|
||||
import { IApplication } from '../../models/application';
|
||||
import EventThemeAPI from '../../api/event-theme';
|
||||
import { SelectOption } from '../../models/select';
|
||||
|
||||
declare const Application: IApplication;
|
||||
|
||||
interface EventThemesProps {
|
||||
event: Event,
|
||||
onChange: (themes: Array<EventTheme>) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* This component shows a select input to edit the themes associated with the event
|
||||
*/
|
||||
export const EventThemes: React.FC<EventThemesProps> = ({ event, onChange }) => {
|
||||
const { t } = useTranslation('shared');
|
||||
|
||||
const [themes, setThemes] = useState<Array<EventTheme>>([]);
|
||||
|
||||
useEffect(() => {
|
||||
EventThemeAPI.index().then(data => setThemes(data));
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Check if there's any EventTheme in DB, otherwise we won't display the selector
|
||||
*/
|
||||
const hasThemes = (): boolean => {
|
||||
return themes.length > 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the current theme(s) for the given event, formatted to match the react-select format
|
||||
*/
|
||||
const defaultValues = (): Array<SelectOption<number>> => {
|
||||
const res = [];
|
||||
themes.forEach(t => {
|
||||
if (event.event_theme_ids && event.event_theme_ids.indexOf(t.id) > -1) {
|
||||
res.push({ value: t.id, label: t.name });
|
||||
}
|
||||
});
|
||||
return res;
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when the selection has changed.
|
||||
* Convert the react-select specific format to an array of EventTheme, and call the provided callback.
|
||||
*/
|
||||
const handleChange = (selectedOptions: Array<SelectOption<number>>): void => {
|
||||
const res = [];
|
||||
selectedOptions.forEach(opt => {
|
||||
res.push(themes.find(t => t.id === opt.value));
|
||||
});
|
||||
onChange(res);
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert all themes to the react-select format
|
||||
*/
|
||||
const buildOptions = (): Array<SelectOption<number>> => {
|
||||
return themes.map(t => {
|
||||
return { value: t.id, label: t.name };
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="event-themes">
|
||||
{hasThemes() && <div className="event-themes--panel">
|
||||
<h3>{ t('app.shared.event_themes.title') }</h3>
|
||||
<div className="content">
|
||||
<Select defaultValue={defaultValues()}
|
||||
placeholder={t('app.shared.event_themes.select_theme')}
|
||||
onChange={handleChange}
|
||||
options={buildOptions()}
|
||||
isMulti />
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const EventThemesWrapper: React.FC<EventThemesProps> = ({ event, onChange }) => {
|
||||
return (
|
||||
<Loader>
|
||||
<EventThemes event={event} onChange={onChange}/>
|
||||
</Loader>
|
||||
);
|
||||
};
|
||||
|
||||
Application.Components.component('eventThemes', react2angular(EventThemesWrapper, ['event', 'onChange']));
|
@ -0,0 +1,66 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Event } from '../../models/event';
|
||||
import { FabModal } from '../base/fab-modal';
|
||||
import { FabAlert } from '../base/fab-alert';
|
||||
|
||||
type EditionMode = 'single' | 'next' | 'all';
|
||||
|
||||
interface UpdateRecurrentModalProps {
|
||||
isOpen: boolean,
|
||||
toggleModal: () => void,
|
||||
event: Event,
|
||||
onConfirmed: (data: Event, mode: EditionMode) => void,
|
||||
datesChanged: boolean,
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask the user for confimation about the update of only the current event or also its recurrences
|
||||
*/
|
||||
export const UpdateRecurrentModal: React.FC<UpdateRecurrentModalProps> = ({ isOpen, toggleModal, event, onConfirmed, datesChanged }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
const [editMode, setEditMode] = useState<EditionMode>(null);
|
||||
|
||||
/**
|
||||
* Callback triggered when the user confirms the update
|
||||
*/
|
||||
const handleConfirmation = () => {
|
||||
onConfirmed(event, editMode);
|
||||
};
|
||||
|
||||
/**
|
||||
* The user cannot confirm unless he chooses an option
|
||||
*/
|
||||
const preventConfirm = () => {
|
||||
return !editMode;
|
||||
};
|
||||
|
||||
return (
|
||||
<FabModal isOpen={isOpen}
|
||||
toggleModal={toggleModal}
|
||||
title={t('app.admin.update_recurrent_modal.title')}
|
||||
className="update-recurrent-modal"
|
||||
onConfirm={handleConfirmation}
|
||||
preventConfirm={preventConfirm()}
|
||||
confirmButton={t('app.admin.update_recurrent_modal.confirm', { MODE: editMode })}
|
||||
closeButton>
|
||||
<p>{t('app.admin.update_recurrent_modal.edit_recurring_event')}</p>
|
||||
<label>
|
||||
<input name="edit_mode" type="radio" value="single" onClick={() => setEditMode('single')} />
|
||||
<span>{t('app.admin.update_recurrent_modal.edit_this_event')}</span>
|
||||
</label>
|
||||
<label>
|
||||
<input name="edit_mode" type="radio" value="next" onClick={() => setEditMode('next')} />
|
||||
<span>{t('app.admin.update_recurrent_modal.edit_this_and_next')}</span>
|
||||
</label>
|
||||
<label>
|
||||
<input name="edit_mode" type="radio" value="all" onClick={() => setEditMode('all')} />
|
||||
<span>{t('app.admin.update_recurrent_modal.edit_all')}</span>
|
||||
</label>
|
||||
{datesChanged && editMode !== 'single' && <FabAlert level="warning">
|
||||
{t('app.admin.update_recurrent_modal.date_wont_change')}
|
||||
</FabAlert>}
|
||||
</FabModal>
|
||||
);
|
||||
};
|
@ -41,126 +41,10 @@
|
||||
*/
|
||||
class EventsController {
|
||||
constructor ($scope, $state) {
|
||||
// default parameters for AngularUI-Bootstrap datepicker
|
||||
$scope.datePicker = {
|
||||
format: Fablab.uibDateFormat,
|
||||
startOpened: false, // default: datePicker is not shown
|
||||
endOpened: false,
|
||||
recurrenceEndOpened: false,
|
||||
options: {
|
||||
startingDay: Fablab.weekStartingDay
|
||||
}
|
||||
};
|
||||
|
||||
// themes of the current event
|
||||
$scope.event_themes = $scope.event.event_theme_ids;
|
||||
|
||||
/**
|
||||
* For use with ngUpload (https://github.com/twilson63/ngUpload).
|
||||
* Intended to be the callback when an upload is done: any raised error will be stacked in the
|
||||
* $scope.alerts array. If everything goes fine, the user is redirected to the project page.
|
||||
* @param content {Object} JSON - The upload's result
|
||||
*/
|
||||
$scope.submited = function (content) {
|
||||
$scope.onSubmited(content);
|
||||
};
|
||||
|
||||
/**
|
||||
* Changes the user's view to the events list page
|
||||
*/
|
||||
$scope.cancel = function () { $state.go('app.public.events_list'); };
|
||||
|
||||
/**
|
||||
* For use with 'ng-class', returns the CSS class name for the uploads previews.
|
||||
* The preview may show a placeholder or the content of the file depending on the upload state.
|
||||
* @param v {*} any attribute, will be tested for truthiness (see JS evaluation rules)
|
||||
*/
|
||||
$scope.fileinputClass = function (v) {
|
||||
if (v) {
|
||||
return 'fileinput-exists';
|
||||
} else {
|
||||
return 'fileinput-new';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This will create a single new empty entry into the event's attachements list.
|
||||
*/
|
||||
$scope.addFile = function () { $scope.event.event_files_attributes.push({}); };
|
||||
|
||||
/**
|
||||
* This will remove the given file from the event's attachements list. If the file was previously uploaded
|
||||
* to the server, it will be marked for deletion on the server. Otherwise, it will be simply truncated from
|
||||
* the attachements array.
|
||||
* @param file {Object} the file to delete
|
||||
*/
|
||||
$scope.deleteFile = function (file) {
|
||||
const index = $scope.event.event_files_attributes.indexOf(file);
|
||||
if (file.id != null) {
|
||||
return file._destroy = true;
|
||||
} else {
|
||||
return $scope.event.event_files_attributes.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Show/Hide the "start" datepicker (open the drop down/close it)
|
||||
*/
|
||||
$scope.toggleStartDatePicker = function ($event) {
|
||||
$event.preventDefault();
|
||||
$event.stopPropagation();
|
||||
return $scope.datePicker.startOpened = !$scope.datePicker.startOpened;
|
||||
};
|
||||
|
||||
/**
|
||||
* Show/Hide the "end" datepicker (open the drop down/close it)
|
||||
*/
|
||||
$scope.toggleEndDatePicker = function ($event) {
|
||||
$event.preventDefault();
|
||||
$event.stopPropagation();
|
||||
return $scope.datePicker.endOpened = !$scope.datePicker.endOpened;
|
||||
};
|
||||
|
||||
/**
|
||||
* Masks/displays the recurrence pane allowing the admin to set the current event as recursive
|
||||
*/
|
||||
$scope.toggleRecurrenceEnd = function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return $scope.datePicker.recurrenceEndOpened = !$scope.datePicker.recurrenceEndOpened;
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize a new price item in the additional prices list
|
||||
*/
|
||||
$scope.addPrice = function () {
|
||||
$scope.event.prices.push({
|
||||
category: null,
|
||||
amount: null
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove the price or mark it as 'to delete'
|
||||
*/
|
||||
$scope.removePrice = function (price, event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (price.id) {
|
||||
price._destroy = true;
|
||||
} else {
|
||||
const index = $scope.event.prices.indexOf(price);
|
||||
$scope.event.prices.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* When the theme selection has changes, extract the IDs to populate the form
|
||||
* @param themes {Array<EventTheme>}
|
||||
*/
|
||||
$scope.handleEventChange = function (themes) {
|
||||
$scope.event_themes = themes.map(t => t.id);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -555,60 +439,22 @@ Application.Controllers.controller('ShowEventReservationsController', ['$scope',
|
||||
/**
|
||||
* Controller used in the event creation page
|
||||
*/
|
||||
Application.Controllers.controller('NewEventController', ['$scope', '$state', 'CSRF', 'categoriesPromise', 'themesPromise', 'ageRangesPromise', 'priceCategoriesPromise', '_t',
|
||||
function ($scope, $state, CSRF, categoriesPromise, themesPromise, ageRangesPromise, priceCategoriesPromise, _t) {
|
||||
Application.Controllers.controller('NewEventController', ['$scope', '$state', 'CSRF', 'growl',
|
||||
function ($scope, $state, CSRF, growl) {
|
||||
CSRF.setMetaTags();
|
||||
|
||||
// API URL where the form will be posted
|
||||
$scope.actionUrl = '/api/events/';
|
||||
|
||||
// Form action on the above URL
|
||||
$scope.method = 'post';
|
||||
|
||||
// List of categories for the events
|
||||
$scope.categories = categoriesPromise;
|
||||
|
||||
// List of events themes
|
||||
$scope.themes = themesPromise;
|
||||
|
||||
// List of age ranges
|
||||
$scope.ageRanges = ageRangesPromise;
|
||||
|
||||
// List of availables price's categories
|
||||
$scope.priceCategories = priceCategoriesPromise;
|
||||
|
||||
// Default event parameters
|
||||
$scope.event = {
|
||||
event_files_attributes: [],
|
||||
start_date: new Date(),
|
||||
end_date: new Date(),
|
||||
start_time: new Date(),
|
||||
end_time: new Date(),
|
||||
all_day: true,
|
||||
recurrence: 'none',
|
||||
category_id: null,
|
||||
prices: []
|
||||
/**
|
||||
* Callback triggered by react components
|
||||
*/
|
||||
$scope.onSuccess = function (message) {
|
||||
growl.success(message);
|
||||
};
|
||||
|
||||
// Possible types of recurrences for an event
|
||||
$scope.recurrenceTypes = [
|
||||
{ label: _t('app.admin.events_new.none'), value: 'none' },
|
||||
{ label: _t('app.admin.events_new.every_days'), value: 'day' },
|
||||
{ label: _t('app.admin.events_new.every_week'), value: 'week' },
|
||||
{ label: _t('app.admin.events_new.every_month'), value: 'month' },
|
||||
{ label: _t('app.admin.events_new.every_year'), value: 'year' }
|
||||
];
|
||||
|
||||
// triggered when the new event form was submitted to the API and have received an answer
|
||||
$scope.onSubmited = function (content) {
|
||||
if ((content.id == null)) {
|
||||
$scope.alerts = [];
|
||||
angular.forEach(content, function (v, k) {
|
||||
angular.forEach(v, function (err) { $scope.alerts.push({ msg: k + ': ' + err, type: 'danger' }); });
|
||||
});
|
||||
} else {
|
||||
$state.go('app.public.events_list');
|
||||
}
|
||||
/**
|
||||
* Callback triggered by react components
|
||||
*/
|
||||
$scope.onError = function (message) {
|
||||
growl.error(message);
|
||||
};
|
||||
|
||||
// Using the EventsController
|
||||
@ -619,109 +465,25 @@ Application.Controllers.controller('NewEventController', ['$scope', '$state', 'C
|
||||
/**
|
||||
* Controller used in the events edition page
|
||||
*/
|
||||
Application.Controllers.controller('EditEventController', ['$scope', '$state', '$transition$', 'CSRF', 'eventPromise', 'categoriesPromise', 'themesPromise', 'ageRangesPromise', 'priceCategoriesPromise', '$uibModal', 'growl', '_t',
|
||||
function ($scope, $state, $transition$, CSRF, eventPromise, categoriesPromise, themesPromise, ageRangesPromise, priceCategoriesPromise, $uibModal, growl, _t) {
|
||||
Application.Controllers.controller('EditEventController', ['$scope', '$state', 'CSRF', 'eventPromise', 'growl',
|
||||
function ($scope, $state, CSRF, eventPromise, growl) {
|
||||
/* PUBLIC SCOPE */
|
||||
|
||||
// API URL where the form will be posted
|
||||
$scope.actionUrl = `/api/events/${$transition$.params().id}`;
|
||||
|
||||
// Form action on the above URL
|
||||
$scope.method = 'put';
|
||||
|
||||
// Retrieve the event details, in case of error the user is redirected to the events listing
|
||||
$scope.event = eventPromise;
|
||||
$scope.event = cleanEvent(eventPromise);
|
||||
|
||||
// We'll keep track of the initial dates here, for later comparison
|
||||
$scope.initialDates = {};
|
||||
|
||||
// List of categories for the events
|
||||
$scope.categories = categoriesPromise;
|
||||
|
||||
// List of available price's categories
|
||||
$scope.priceCategories = priceCategoriesPromise;
|
||||
|
||||
// List of events themes
|
||||
$scope.themes = themesPromise;
|
||||
|
||||
// List of age ranges
|
||||
$scope.ageRanges = ageRangesPromise;
|
||||
|
||||
// Default edit-mode for periodic event
|
||||
$scope.editMode = 'single';
|
||||
|
||||
// show edit-mode modal if event is recurrent
|
||||
$scope.isShowEditModeModal = $scope.event.recurrence_events.length > 0;
|
||||
|
||||
$scope.editRecurrent = function (e) {
|
||||
if ($scope.isShowEditModeModal && $scope.event.recurrence_events.length > 0) {
|
||||
e.preventDefault();
|
||||
|
||||
// open a choice edit-mode dialog
|
||||
const modalInstance = $uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: '/events/editRecurrent.html',
|
||||
size: 'md',
|
||||
controller: 'EditRecurrentEventController',
|
||||
resolve: {
|
||||
editMode: function () { return $scope.editMode; },
|
||||
initialDates: function () { return $scope.initialDates; },
|
||||
currentEvent: function () { return $scope.event; }
|
||||
}
|
||||
});
|
||||
// submit form event by edit-mode
|
||||
modalInstance.result.then(function (res) {
|
||||
$scope.isShowEditModeModal = false;
|
||||
$scope.editMode = res.editMode;
|
||||
e.target.click();
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Callback triggered by react components
|
||||
*/
|
||||
$scope.onSuccess = function (message) {
|
||||
growl.success(message);
|
||||
};
|
||||
|
||||
// triggered when the edit event form was submitted to the API and have received an answer
|
||||
$scope.onSubmited = function (data) {
|
||||
if (data.total === data.updated) {
|
||||
if (data.updated > 1) {
|
||||
growl.success(_t(
|
||||
'app.admin.events_edit.events_updated',
|
||||
{ COUNT: data.updated - 1 }
|
||||
));
|
||||
} else {
|
||||
growl.success(_t(
|
||||
'app.admin.events_edit.event_successfully_updated'
|
||||
));
|
||||
}
|
||||
} else {
|
||||
if (data.total > 1) {
|
||||
growl.warning(_t(
|
||||
'app.admin.events_edit.events_not_updated',
|
||||
{ TOTAL: data.total, COUNT: data.total - data.updated }
|
||||
));
|
||||
if (_.find(data.details, { error: 'EventPriceCategory' })) {
|
||||
growl.error(_t(
|
||||
'app.admin.events_edit.error_deleting_reserved_price'
|
||||
));
|
||||
} else {
|
||||
growl.error(_t(
|
||||
'app.admin.events_edit.other_error'
|
||||
));
|
||||
}
|
||||
} else {
|
||||
growl.error(_t(
|
||||
'app.admin.events_edit.unable_to_update_the_event'
|
||||
));
|
||||
if (data.details[0].error === 'EventPriceCategory') {
|
||||
growl.error(_t(
|
||||
'app.admin.events_edit.error_deleting_reserved_price'
|
||||
));
|
||||
} else {
|
||||
growl.error(_t(
|
||||
'app.admin.events_edit.other_error'
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
$state.go('app.public.events_list');
|
||||
/**
|
||||
* Callback triggered by react components
|
||||
*/
|
||||
$scope.onError = function (message) {
|
||||
growl.error(message);
|
||||
};
|
||||
|
||||
/* PRIVATE SCOPE */
|
||||
@ -732,19 +494,17 @@ Application.Controllers.controller('EditEventController', ['$scope', '$state', '
|
||||
const initialize = function () {
|
||||
CSRF.setMetaTags();
|
||||
|
||||
// init the dates to JS objects
|
||||
$scope.event.start_date = moment($scope.event.start_date).toDate();
|
||||
$scope.event.end_date = moment($scope.event.end_date).toDate();
|
||||
|
||||
$scope.initialDates = {
|
||||
start: new Date($scope.event.start_date.valueOf()),
|
||||
end: new Date($scope.event.end_date.valueOf())
|
||||
};
|
||||
|
||||
// Using the EventsController
|
||||
return new EventsController($scope, $state);
|
||||
};
|
||||
|
||||
// prepare the event for the react-hook-form
|
||||
function cleanEvent (event) {
|
||||
delete event.$promise;
|
||||
delete event.$resolved;
|
||||
return event;
|
||||
}
|
||||
|
||||
// !!! MUST BE CALLED AT THE END of the controller
|
||||
return initialize();
|
||||
}
|
||||
|
@ -639,7 +639,7 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
|
||||
slot_id: event.availability.slot_id
|
||||
});
|
||||
|
||||
for (let evt_px_cat of Array.from(event.prices)) {
|
||||
for (let evt_px_cat of Array.from(event.event_price_categories_attributes)) {
|
||||
const booked = reserve.tickets[evt_px_cat.id];
|
||||
if (booked > 0) {
|
||||
reservation.tickets_attributes.push({
|
||||
@ -684,7 +684,7 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
|
||||
totalSeats: 0
|
||||
};
|
||||
|
||||
for (let evt_px_cat of Array.from($scope.event.prices)) {
|
||||
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.tickets[evt_px_cat.id] = 0;
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import moment, { unitOfTime } from 'moment';
|
||||
import { IFablab } from '../models/fablab';
|
||||
import { TDateISO } from '../typings/date-iso';
|
||||
import { TDateISO, TDateISODate, THours, TMinutes } from '../typings/date-iso';
|
||||
|
||||
declare let Fablab: IFablab;
|
||||
|
||||
@ -8,15 +8,24 @@ export default class FormatLib {
|
||||
/**
|
||||
* Return the formatted localized date for the given date
|
||||
*/
|
||||
static date = (date: Date|TDateISO): string => {
|
||||
static date = (date: Date|TDateISO|TDateISODate): string => {
|
||||
return Intl.DateTimeFormat().format(moment(date).toDate());
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the formatted localized time for the given date
|
||||
*/
|
||||
static time = (date: Date|TDateISO): string => {
|
||||
return Intl.DateTimeFormat(Fablab.intl_locale, { hour: 'numeric', minute: 'numeric' }).format(moment(date).toDate());
|
||||
static time = (date: Date|TDateISO|`${THours}:${TMinutes}`): string => {
|
||||
let tempDate: Date;
|
||||
const isoTimeMatch = (date as string).match(/^(\d\d):(\d\d)$/);
|
||||
if (isoTimeMatch) {
|
||||
tempDate = new Date();
|
||||
tempDate.setHours(parseInt(isoTimeMatch[1], 10));
|
||||
tempDate.setMinutes(parseInt(isoTimeMatch[2], 10));
|
||||
} else {
|
||||
tempDate = moment(date).toDate();
|
||||
}
|
||||
return Intl.DateTimeFormat(Fablab.intl_locale, { hour: 'numeric', minute: 'numeric' }).format(tempDate);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -37,6 +46,6 @@ export default class FormatLib {
|
||||
* Return currency symbol for currency setting
|
||||
*/
|
||||
static currencySymbol = (): string => {
|
||||
return new Intl.NumberFormat('fr', { style: 'currency', currency: Fablab.intl_currency }).formatToParts()[2].value;
|
||||
return new Intl.NumberFormat(Fablab.intl_locale, { style: 'currency', currency: Fablab.intl_currency }).formatToParts().filter(p => p.type === 'currency')[0].value;
|
||||
};
|
||||
}
|
||||
|
@ -1,5 +0,0 @@
|
||||
export interface EventTheme {
|
||||
id: number,
|
||||
name: string,
|
||||
related_to: number
|
||||
}
|
@ -1,52 +1,110 @@
|
||||
import { TDateISO } from '../typings/date-iso';
|
||||
import { TDateISO, TDateISODate, THours, TMinutes } from '../typings/date-iso';
|
||||
import { FileType } from './file';
|
||||
|
||||
export interface EventPriceCategoryAttributes {
|
||||
id?: number,
|
||||
price_category_id: number,
|
||||
amount: number,
|
||||
_destroy?: boolean,
|
||||
category: EventPriceCategory
|
||||
}
|
||||
|
||||
export type RecurrenceOption = 'none' | 'day' | 'week' | 'month' | 'year';
|
||||
|
||||
export interface Event {
|
||||
id: number,
|
||||
id?: number,
|
||||
title: string,
|
||||
description: string,
|
||||
event_image: string,
|
||||
event_files_attributes: Array<{
|
||||
id: number,
|
||||
attachment: string,
|
||||
attachment_url: string
|
||||
}>,
|
||||
event_image_attributes: FileType,
|
||||
event_files_attributes: Array<FileType>,
|
||||
category_id: number,
|
||||
category: {
|
||||
id: number,
|
||||
name: string,
|
||||
slug: string
|
||||
},
|
||||
event_theme_ids: Array<number>,
|
||||
event_themes: Array<{
|
||||
event_theme_ids?: Array<number>,
|
||||
event_themes?: Array<{
|
||||
name: string
|
||||
}>,
|
||||
age_range_id: number,
|
||||
age_range: {
|
||||
age_range_id?: number,
|
||||
age_range?: {
|
||||
name: string
|
||||
},
|
||||
start_date: TDateISO,
|
||||
start_time: TDateISO,
|
||||
end_date: TDateISO,
|
||||
end_time: TDateISO,
|
||||
month: string;
|
||||
month_id: number,
|
||||
year: number,
|
||||
all_day: boolean,
|
||||
availability: {
|
||||
start_date: TDateISODate | Date,
|
||||
start_time: `${THours}:${TMinutes}`,
|
||||
end_date: TDateISODate | Date,
|
||||
end_time: `${THours}:${TMinutes}`,
|
||||
month?: string;
|
||||
month_id?: number,
|
||||
year?: number,
|
||||
all_day?: boolean,
|
||||
availability?: {
|
||||
id: number,
|
||||
start_at: TDateISO,
|
||||
end_at: TDateISO
|
||||
},
|
||||
availability_id: number,
|
||||
amount: number,
|
||||
prices: Array<{
|
||||
id: number,
|
||||
amount: number,
|
||||
category: {
|
||||
id: number,
|
||||
name: string
|
||||
}
|
||||
}>,
|
||||
event_price_categories_attributes?: Array<EventPriceCategoryAttributes>,
|
||||
nb_total_places: number,
|
||||
nb_free_places: number
|
||||
nb_free_places: number,
|
||||
recurrence_id?: number,
|
||||
updated_at?: TDateISO,
|
||||
recurrence_events?: Array<{
|
||||
id: number,
|
||||
start_date: TDateISODate,
|
||||
start_time: `${THours}:${TMinutes}`
|
||||
end_date: TDateISODate
|
||||
end_time: `${THours}:${TMinutes}`
|
||||
nb_free_places: number,
|
||||
availability_id: number
|
||||
}>,
|
||||
recurrence: RecurrenceOption,
|
||||
recurrence_end_at: Date
|
||||
}
|
||||
|
||||
export interface EventDecoration {
|
||||
id?: number,
|
||||
name: string,
|
||||
related_to?: number
|
||||
}
|
||||
|
||||
export type EventTheme = EventDecoration;
|
||||
export type EventCategory = EventDecoration;
|
||||
export type AgeRange = EventDecoration;
|
||||
|
||||
export interface EventPriceCategory {
|
||||
id?: number,
|
||||
name: string,
|
||||
conditions?: string,
|
||||
events?: number,
|
||||
created_at?: TDateISO
|
||||
}
|
||||
|
||||
export interface EventUpdateResult {
|
||||
action: 'update',
|
||||
total: number,
|
||||
updated: number,
|
||||
details: {
|
||||
events: Array<{
|
||||
event: Event,
|
||||
status: boolean,
|
||||
error?: string,
|
||||
message?: string
|
||||
}>,
|
||||
slots: Array<{
|
||||
slot: {
|
||||
id: number,
|
||||
availability_id: number,
|
||||
created_at: TDateISO,
|
||||
end_at: TDateISO,
|
||||
start_at: TDateISO,
|
||||
updated_at: TDateISO,
|
||||
},
|
||||
status: boolean,
|
||||
error?: string,
|
||||
message?: string
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
@ -801,12 +801,6 @@ angular.module('application.router', ['ui.router'])
|
||||
templateUrl: '/events/new.html',
|
||||
controller: 'NewEventController'
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
categoriesPromise: ['Category', function (Category) { return Category.query().$promise; }],
|
||||
themesPromise: ['EventTheme', function (EventTheme) { return EventTheme.query().$promise; }],
|
||||
ageRangesPromise: ['AgeRange', function (AgeRange) { return AgeRange.query().$promise; }],
|
||||
priceCategoriesPromise: ['PriceCategory', function (PriceCategory) { return PriceCategory.query().$promise; }]
|
||||
}
|
||||
})
|
||||
.state('app.admin.events_edit', {
|
||||
@ -818,11 +812,7 @@ angular.module('application.router', ['ui.router'])
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
eventPromise: ['Event', '$transition$', function (Event, $transition$) { return Event.get({ id: $transition$.params().id }).$promise; }],
|
||||
categoriesPromise: ['Category', function (Category) { return Category.query().$promise; }],
|
||||
themesPromise: ['EventTheme', function (EventTheme) { return EventTheme.query().$promise; }],
|
||||
ageRangesPromise: ['AgeRange', function (AgeRange) { return AgeRange.query().$promise; }],
|
||||
priceCategoriesPromise: ['PriceCategory', function (PriceCategory) { return PriceCategory.query().$promise; }]
|
||||
eventPromise: ['Event', '$transition$', function (Event, $transition$) { return Event.get({ id: $transition$.params().id }).$promise; }]
|
||||
}
|
||||
})
|
||||
.state('app.admin.event_reservations', {
|
||||
|
@ -39,6 +39,8 @@
|
||||
@import "modules/dashboard/reservations/reservations-dashboard";
|
||||
@import "modules/dashboard/reservations/reservations-panel";
|
||||
@import "modules/events/event";
|
||||
@import "modules/events/event-form";
|
||||
@import "modules/events/update-recurrent-modal";
|
||||
@import "modules/form/abstract-form-item";
|
||||
@import "modules/form/form-input";
|
||||
@import "modules/form/form-multi-file-upload";
|
||||
|
37
app/frontend/src/stylesheets/modules/events/event-form.scss
Normal file
37
app/frontend/src/stylesheets/modules/events/event-form.scss
Normal file
@ -0,0 +1,37 @@
|
||||
.event-form {
|
||||
.additional-prices {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
|
||||
.add-price {
|
||||
max-width: fit-content;
|
||||
margin-bottom: 1.6rem;
|
||||
}
|
||||
.price-item {
|
||||
width: 100%;
|
||||
.remove-price {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin: 2.6rem 2rem 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.destroyed-item {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.dates, .times, .price-item, .recurring {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.form-item:first-child {
|
||||
margin-right: 32px;
|
||||
}
|
||||
}
|
||||
.submit-btn {
|
||||
margin-top: 2rem;
|
||||
float: right;
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
.update-recurrent-modal {
|
||||
label {
|
||||
margin-left: 2rem;
|
||||
|
||||
span {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,280 +0,0 @@
|
||||
<div class="row no-gutter">
|
||||
|
||||
<div class=" col-sm-12 col-md-12 col-lg-8 nopadding">
|
||||
|
||||
<section class="panel panel-default bg-light m-lg">
|
||||
<div class="panel-body m-r">
|
||||
|
||||
<uib-alert ng-repeat="alert in alerts" type="{{alert.type}}" close="closeAlert($index)">{{alert.msg}}</uib-alert>
|
||||
|
||||
<input name="_method" type="hidden" ng-value="method">
|
||||
|
||||
<div class="form-group" ng-class="{'has-error': eventForm['event[title]'].$dirty && eventForm['event[title]'].$invalid}">
|
||||
<label for="event_title" class="col-sm-3 control-label">{{ 'app.shared.event.title' | translate }} *</label>
|
||||
<div class="col-sm-9">
|
||||
<input ng-model="event.title" type="text" name="event[title]" class="form-control" id="event_title" placeholder="" required>
|
||||
<span class="help-block" ng-show="eventForm['event[title]'].$dirty && eventForm['event[title]'].$error.required" translate>{{ 'app.shared.event.title_is_required' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-group">
|
||||
<label for="event_image" class="col-sm-3 control-label" translate>{{ 'app.shared.event.matching_visual' }}</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="fileinput" data-provides="fileinput" ng-class="fileinputClass(event.event_image)">
|
||||
<div class="fileinput-new thumbnail" style="width: 334px; height: 250px;">
|
||||
<img src="data:image/png;base64," data-src="holder.js/100%x100%/text:/font:'Font Awesome 5 Free'/icon" bs-holder ng-if="!event.event_image">
|
||||
</div>
|
||||
<div class="fileinput-preview fileinput-exists thumbnail" data-trigger="fileinput" style="max-width: 334px;">
|
||||
<img ng-src="{{ event.event_image }}" alt="" />
|
||||
</div>
|
||||
<div>
|
||||
<span class="btn btn-default btn-file"><span class="fileinput-new">{{ 'app.shared.event.choose_a_picture' | translate }} <i class="fa fa-upload fa-fw"></i></span><span class="fileinput-exists" translate>{{ 'app.shared.buttons.change' }}</span>
|
||||
<input type="file" name="event[event_image_attributes][attachment]" accept="image/jpeg,image/gif,image/png"></span>
|
||||
<a class="btn btn-danger fileinput-exists" data-dismiss="fileinput"><i class="fa fa-trash-o"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{'has-error': eventForm['event[description]'].$dirty && eventForm['event[description]'].$invalid}">
|
||||
<label for="description" class="col-sm-3 control-label">{{ 'app.shared.event.description' | translate }} *</label>
|
||||
<div class="col-sm-9">
|
||||
<input type="hidden"
|
||||
name="event[description]"
|
||||
ng-value="event.description" />
|
||||
<summernote ng-model="event.description"
|
||||
id="event_description"
|
||||
placeholder=""
|
||||
config="summernoteOpts"
|
||||
name="event[description]"
|
||||
required>
|
||||
</summernote>
|
||||
<span class="help-block" ng-show="eventForm['event[description]'].$dirty && eventForm['event[description]'].$error.required" translate>{{ 'app.shared.event.description_is_required' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label" translate>{{ 'app.shared.event.attachments' }}</label>
|
||||
<div class="col-sm-9">
|
||||
<div ng-repeat="file in event.event_files_attributes" ng-show="!file._destroy">
|
||||
<input type="hidden" name="event[event_files_attributes][][id]" ng-value="file.id" />
|
||||
<input type="hidden" name="event[event_files_attributes][][_destroy]" ng-value="file._destroy" />
|
||||
|
||||
<div class="fileinput input-group" data-provides="fileinput" ng-class="fileinputClass(file.attachment)">
|
||||
<div class="form-control" data-trigger="fileinput">
|
||||
<i class="glyphicon glyphicon-file fileinput-exists"></i> <span class="fileinput-filename">{{file.attachment}}</span>
|
||||
</div>
|
||||
<span class="input-group-addon btn btn-default btn-file"><span class="fileinput-new" translate>{{ 'app.shared.buttons.browse' }}</span>
|
||||
<span class="fileinput-exists" translate>{{ 'app.shared.buttons.change' }}</span><input type="file" name="event[event_files_attributes][][attachment]" accept="application/pdf"></span>
|
||||
<a class="input-group-addon btn btn-danger fileinput-exists" data-dismiss="fileinput" ng-click="deleteFile(file)"><i class="fa fa-trash-o"></i></a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<a class="btn btn-default" ng-click="addFile()" role="button">{{ 'app.shared.event.add_a_new_file' | translate }} <i class="fa fa-file-o fa-fw"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div> <!-- ./panel-body -->
|
||||
<input ng-model="editMode" type="hidden" name="edit_mode">
|
||||
<div class="panel-footer no-padder">
|
||||
<input type="submit"
|
||||
ng-value="submitName"
|
||||
class="r-b btn-valid btn btn-warning btn-block p-lg btn-lg text-u-c"
|
||||
ng-disabled="eventForm.$invalid || event.category_id === null"
|
||||
ng-click="editRecurrent($event)"/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 col-md-12 col-lg-4">
|
||||
|
||||
|
||||
<div class="widget panel b-a m m-t-lg">
|
||||
<div class="panel-heading b-b small">
|
||||
<h3 translate>{{ 'app.shared.event.event_type' }} *</h3>
|
||||
</div>
|
||||
<div class="widget-content no-bg wrapper">
|
||||
<ui-select ng-model="event.category_id" name="event[category_id][]">
|
||||
<ui-select-match>
|
||||
<span ng-bind="$select.selected.name"></span>
|
||||
<input type="hidden" name="event[category_id]" value="{{$select.selected.id}}" />
|
||||
</ui-select-match>
|
||||
<ui-select-choices repeat="c.id as c in (categories | filter: $select.search)">
|
||||
<span ng-bind-html="c.name | highlight: $select.search"></span>
|
||||
</ui-select-choices>
|
||||
</ui-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<event-themes event="event" on-change="handleEventChange"></event-themes>
|
||||
<input type="hidden" name="event[event_theme_ids][]" ng-repeat="id in event_themes" value="{{id}}" />
|
||||
<input type="hidden" name="event[event_theme_ids][]" value="" ng-if="event_themes.length === 0" />
|
||||
|
||||
<div class="widget panel b-a m m-t-lg" ng-show="ageRanges.length > 0">
|
||||
<div class="panel-heading b-b small">
|
||||
<h3 translate>{{ 'app.shared.event.age_range' }}</h3>
|
||||
</div>
|
||||
<div class="widget-content no-bg wrapper">
|
||||
<ui-select ng-model="event.age_range_id" name="event[age_range_id][]">
|
||||
<ui-select-match>
|
||||
<span ng-bind="$select.selected.name"></span>
|
||||
<input type="hidden" name="event[age_range_id]" value="{{$select.selected.id}}" />
|
||||
</ui-select-match>
|
||||
<ui-select-choices repeat="a.id as a in (ageRanges | filter: $select.search)">
|
||||
<span ng-bind-html="a.name | highlight: $select.search"></span>
|
||||
</ui-select-choices>
|
||||
</ui-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="widget panel b-a m m-t-lg">
|
||||
<div class="panel-heading b-b small">
|
||||
<h3 translate>{{ 'app.shared.event.dates_and_opening_hours' }}</h3>
|
||||
</div>
|
||||
<div class="widget-content no-bg wrapper">
|
||||
<div class="m-b">
|
||||
<label class="v-bottom" translate>{{ 'app.shared.event.all_day' }}</label>
|
||||
<div class="inline v-top">
|
||||
<label class="checkbox-inline">
|
||||
<input type="radio" name="event[all_day]" ng-model="event.all_day" ng-value="true" required/> {{ 'app.shared.buttons.yes' | translate }}
|
||||
</label>
|
||||
<label class="checkbox-inline">
|
||||
<input type="radio" name="event[all_day]" ng-model="event.all_day" ng-value="false"/> {{ 'app.shared.buttons.no' | translate }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="event[availability_id]" ng-value="event.availability_id" ng-if="event.availability_id">
|
||||
<div class="m-b">
|
||||
<label translate>{{ 'app.shared.event.start_date' }}</label>
|
||||
<div class="input-group">
|
||||
<input type="hidden" name="event[start_date]" ng-value="event.start_date">
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
ng-model="event.start_date"
|
||||
uib-datepicker-popup="{{datePicker.format}}"
|
||||
datepicker-options="datePicker.options"
|
||||
is-open="datePicker.startOpened"
|
||||
ng-click="toggleStartDatePicker($event)"
|
||||
required/>
|
||||
<span class="input-group-btn">
|
||||
<button type="button" class="btn btn-default" ng-click="toggleStartDatePicker($event)"><i class="fa fa-calendar"></i></button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="m-b">
|
||||
<label translate>{{ 'app.shared.event.end_date' }}</label>
|
||||
<div class="input-group">
|
||||
<input type="hidden" name="event[end_date]" ng-value="event.end_date">
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
ng-model="event.end_date"
|
||||
uib-datepicker-popup="{{datePicker.format}}"
|
||||
datepicker-options="datePicker.options"
|
||||
is-open="datePicker.endOpened"
|
||||
ng-click="toggleEndDatePicker($event)"
|
||||
required/>
|
||||
<span class="input-group-btn">
|
||||
<button type="button" class="btn btn-default" ng-click="toggleEndDatePicker($event)"><i class="fa fa-calendar"></i></button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="m-b row" ng-if="event.all_day === false">
|
||||
<div class="col-xs-6">
|
||||
<label translate>{{ 'app.shared.event.start_time' }}</label>
|
||||
<div>
|
||||
<input type="hidden" name="event[start_time]" ng-value="event.start_time">
|
||||
<uib-timepicker ng-model="event.start_time" mousewheel="false" hour-step="1" minute-step="1" show-meridian="ismeridian"></uib-timepicker>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-6">
|
||||
<label translate>{{ 'app.shared.event.end_time' }}</label>
|
||||
<div>
|
||||
<input type="hidden" name="event[end_time]" ng-value="event.end_time">
|
||||
<uib-timepicker ng-model="event.end_time" mousewheel="false" hour-step="1" minute-step="1" show-meridian="ismeridian"></uib-timepicker>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="method == 'post'" class="m-b">
|
||||
<label translate>{{ 'app.shared.event.recurrence' }}</label>
|
||||
<select ng-model="event.recurrence" class="form-control" name="event[recurrence]">
|
||||
<option value="{{t.value}}" ng-repeat="t in recurrenceTypes">{{t.label}}</option>
|
||||
</select>
|
||||
<div ng-if="event.recurrence != 'none'">
|
||||
{{ 'app.shared.event._and_ends_on' | translate }}
|
||||
<div class="input-group">
|
||||
<input type="hidden" name="event[recurrence_end_at]" ng-value="event.recurrence_end_at">
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
uib-datepicker-popup="{{datePicker.format}}"
|
||||
datepicker-options="datePicker.options"
|
||||
ng-model="event.recurrence_end_at"
|
||||
is-open="datePicker.recurrenceEndOpened"
|
||||
ng-required="true"/>
|
||||
<span class="input-group-btn">
|
||||
<button type="button" class="btn btn-default" ng-click="toggleRecurrenceEnd($event)"><i class="fa fa-calendar"></i></button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="widget panel b-a m m-t-lg">
|
||||
<div class="panel-heading b-b small">
|
||||
<h3 translate>{{ 'app.shared.event.prices_and_availabilities' }}</h3>
|
||||
</div>
|
||||
<div class="widget-content no-bg wrapper">
|
||||
<div class="form-group">
|
||||
<label for="event_amount" class="col-sm-5 control-label" translate>{{ 'app.shared.event.standard_rate' }}</label>
|
||||
<div class="col-sm-6 p-h-0">
|
||||
<div class="input-group">
|
||||
<input ng-model="event.amount" type="number" name="event[amount]" class="form-control" id="event_amount" required>
|
||||
<div class="input-group-addon">{{currencySymbol}}</div>
|
||||
</div>
|
||||
<span class="help-block" translate>{{ 'app.shared.event.0_equal_free' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-repeat="price in event.prices" ng-show="!price._destroy">
|
||||
<div class="col-sm-5">
|
||||
<input type="hidden" name="event[event_price_categories_attributes][][id]" ng-value="price.id">
|
||||
<select class="form-control"
|
||||
ng-model="price.category"
|
||||
name="event[event_price_categories_attributes][][price_category_id]"
|
||||
ng-options="cat as cat.name for cat in priceCategories track by cat.id">
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-6 p-h-0">
|
||||
<div class="input-group">
|
||||
<input ng-model="price.amount"
|
||||
type="number"
|
||||
name="event[event_price_categories_attributes][][amount]"
|
||||
class="form-control">
|
||||
<div class="input-group-addon">{{currencySymbol}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-1">
|
||||
<input type="hidden" name="event[event_price_categories_attributes][][_destroy]" ng-value="price._destroy">
|
||||
<a class="btn p-h-0" ng-click="removePrice(price, $event)"><i class="fa fa-times text-danger"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="link-icon m-b" ng-hide="event.prices.length == priceCategories.length">
|
||||
<div class="col-sm-offset-5">
|
||||
<i class="fa fa-plus"></i> <span ng-click="addPrice()" translate>{{ 'app.shared.event.add_price' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="event_nb_total_places" class="col-sm-5 control-label" translate>{{ 'app.shared.event.tickets_available' }}</label>
|
||||
<div class="col-sm-6 p-h-0">
|
||||
<div class="input-group">
|
||||
<input ng-model="event.nb_total_places" type="number" name="event[nb_total_places]" class="form-control" id="event_nb_total_places">
|
||||
<div class="input-group-addon"><i class="fa fa-ticket"></i> </div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -16,10 +16,14 @@
|
||||
</section>
|
||||
|
||||
|
||||
<form role="form" name="eventForm" class="form-horizontal" novalidate action="{{ actionUrl }}" ng-upload="submited(content)" upload-options-enable-rails-csrf="true" upload-options-convert-hidden="true">
|
||||
|
||||
<ng-include src="'/events/_form.html'"></ng-include>
|
||||
|
||||
</form>
|
||||
<div class="row no-gutter" >
|
||||
|
||||
<div class="col-md-9 b-r nopadding">
|
||||
<div class="panel panel-default bg-light m-lg">
|
||||
<div class="panel-body m-r">
|
||||
<event-form action="'update'" event="event" on-success="onSuccess" on-error="onError"></event-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,26 +0,0 @@
|
||||
<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>{{ 'app.admin.events_edit.confirmation_required' }}</h1>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p translate>{{ 'app.admin.events_edit.edit_recurring_event' }}</p>
|
||||
<div class="form-group">
|
||||
<label class="checkbox">
|
||||
<input type="radio" name="edit_mode" ng-model="editMode" value="single" required/>
|
||||
<span translate>{{ 'app.admin.events_edit.edit_this_event' }}</span>
|
||||
</label>
|
||||
<label class="checkbox">
|
||||
<input type="radio" name="edit_mode" ng-model="editMode" value="next" required/>
|
||||
<span translate>{{ 'app.admin.events_edit.edit_this_and_next' }}</span>
|
||||
</label>
|
||||
<label class="checkbox">
|
||||
<input type="radio" name="edit_mode" ng-model="editMode" value="all" required/>
|
||||
<span translate>{{ 'app.admin.events_edit.edit_all' }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<p class="alert alert-warning" ng-show="hasDateChanged() && editMode !== 'single'" translate>{{ 'app.admin.events_edit.date_wont_change' }}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-info" ng-click="ok()" translate>{{ 'app.shared.buttons.apply' }}</button>
|
||||
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'app.shared.buttons.cancel' }}</button>
|
||||
</div>
|
@ -16,10 +16,12 @@
|
||||
</section>
|
||||
|
||||
|
||||
<form role="form" name="eventForm" class="form-horizontal" novalidate action="{{ actionUrl }}" ng-upload="submited(content)" upload-options-enable-rails-csrf="true">
|
||||
|
||||
<ng-include src="'/events/_form.html'"></ng-include>
|
||||
|
||||
</form>
|
||||
<div class="col-md-9 b-r nopadding">
|
||||
<div class="panel panel-default bg-light m-lg">
|
||||
<div class="panel-body m-r">
|
||||
<event-form action="'create'" on-success="onSuccess" on-error="onError"></event-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
@ -31,8 +31,8 @@
|
||||
|
||||
<div class="article wrapper-lg">
|
||||
|
||||
<div class="article-thumbnail" ng-if="event.event_image">
|
||||
<img ng-src="{{event.event_image}}" alt="{{event.title}}" class="img-responsive">
|
||||
<div class="article-thumbnail" ng-if="event.event_image_attributes">
|
||||
<img ng-src="{{event.event_image_attributes.attachment_url}}" alt="{{event.title}}" class="img-responsive">
|
||||
</div>
|
||||
|
||||
<h3 translate>{{ 'app.public.events_show.event_description' }}</h3>
|
||||
@ -57,7 +57,7 @@
|
||||
|
||||
<ul class="widget-content list-group list-group-lg no-bg auto">
|
||||
<li ng-repeat="file in event.event_files_attributes" class="list-group-item no-b clearfix">
|
||||
<a target="_blank" ng-href="{{file.attachment_url}}"><i class="fa fa-arrow-circle-o-down"> </i> {{file.attachment | humanize : 25}}</a>
|
||||
<a target="_blank" ng-href="{{file.attachment_url}}"><i class="fa fa-arrow-circle-o-down"> </i> {{file.attachment_name | humanize : 25}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
@ -85,12 +85,12 @@
|
||||
<dd>{{ 'app.public.events_show.beginning' | translate }} <span class="text-u-l">{{event.start_date | amDateFormat:'L'}}</span><br>{{ 'app.public.events_show.ending' | translate }} <span class="text-u-l">{{event.end_date | amDateFormat:'L'}}</span></dd>
|
||||
<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_date | amDateFormat:'LT'}}</span> {{ 'app.public.events_show.to_time' | translate }} <span class="text-u-l">{{event.end_date | amDateFormat:'LT'}}</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>
|
||||
</dl>
|
||||
|
||||
<div class="text-sm" ng-if="event.amount">
|
||||
<div>{{ 'app.public.events_show.full_price_' | translate }} <span>{{ event.amount | currency}}</span></div>
|
||||
<div ng-repeat="price in event.prices" class="description-hover">
|
||||
<div ng-repeat="price in event.event_price_categories_attributes" class="description-hover">
|
||||
<span uib-popover="{{getPriceCategoryConditions(price.category.id)}}" popover-trigger="mouseenter">
|
||||
{{price.category.name}} :
|
||||
</span>
|
||||
@ -120,7 +120,7 @@
|
||||
</select> {{ 'app.public.events_show.ticket' | translate:{NUMBER:reserve.nbReservePlaces} }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" ng-repeat="price in event.prices">
|
||||
<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]">
|
||||
|
@ -1,6 +1,6 @@
|
||||
user_is_admin = (current_user and current_user.admin?)
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.array!(@categories) do |category|
|
||||
json.extract! category, :id, :name
|
||||
json.related_to category.events.count if user_is_admin
|
||||
json.related_to category.events.count if current_user&.admin?
|
||||
end
|
||||
|
@ -1,10 +1,16 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.extract! event, :id, :title, :description
|
||||
json.event_image event.event_image.attachment_url if event.event_image
|
||||
if event.event_image
|
||||
json.event_image_attributes do
|
||||
json.id event.event_image.id
|
||||
json.attachment_name event.event_image.attachment_identifier
|
||||
json.attachment_url event.event_image.attachment_url
|
||||
end
|
||||
end
|
||||
json.event_files_attributes event.event_files do |f|
|
||||
json.id f.id
|
||||
json.attachment f.attachment_identifier
|
||||
json.attachment_name f.attachment_identifier
|
||||
json.attachment_url f.attachment_url
|
||||
end
|
||||
json.category_id event.category_id
|
||||
@ -25,10 +31,10 @@ if event.age_range
|
||||
json.name event.age_range.name
|
||||
end
|
||||
end
|
||||
json.start_date event.availability.start_at
|
||||
json.start_time event.availability.start_at
|
||||
json.end_date event.availability.end_at
|
||||
json.end_time event.availability.end_at
|
||||
json.start_date event.availability.start_at.to_date
|
||||
json.start_time event.availability.start_at.strftime('%R')
|
||||
json.end_date event.availability.end_at.to_date
|
||||
json.end_time event.availability.end_at.strftime('%R')
|
||||
json.month t('date.month_names')[event.availability.start_at.month]
|
||||
json.month_id event.availability.start_at.month
|
||||
json.year event.availability.start_at.year
|
||||
@ -40,10 +46,11 @@ json.availability do
|
||||
json.slot_id event.availability.slots.first&.id
|
||||
end
|
||||
json.availability_id event.availability_id
|
||||
json.amount (event.amount / 100.0) if event.amount
|
||||
json.prices event.event_price_categories do |p_cat|
|
||||
json.amount event.amount / 100.0 if event.amount
|
||||
json.event_price_categories_attributes event.event_price_categories do |p_cat|
|
||||
json.id p_cat.id
|
||||
json.amount (p_cat.amount / 100.0)
|
||||
json.price_category_id p_cat.price_category.id
|
||||
json.amount p_cat.amount / 100.0
|
||||
json.category do
|
||||
json.extract! p_cat.price_category, :id, :name
|
||||
end
|
||||
|
@ -1,8 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
total = @events.except(:offset, :limit, :order).count
|
||||
|
||||
json.array!(@events) do |event|
|
||||
json.partial! 'api/events/event', event: event
|
||||
json.event_image_small event.event_image.attachment.small.url if event.event_image
|
||||
|
||||
|
||||
json.nb_total_events total
|
||||
end
|
||||
|
@ -1,10 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.partial! 'api/events/event', event: @event
|
||||
json.recurrence_events @event.recurrence_events do |e|
|
||||
json.id e.id
|
||||
json.start_date e.availability.start_at
|
||||
json.start_time e.availability.start_at
|
||||
json.end_date e.availability.end_at
|
||||
json.end_time e.availability.end_at
|
||||
json.start_date e.availability.start_at.to_date
|
||||
json.start_time e.availability.start_at.strftime('%R')
|
||||
json.end_date e.availability.end_at.to_date
|
||||
json.end_time e.availability.end_at.strftime('%R')
|
||||
json.nb_free_places e.nb_free_places
|
||||
json.availability_id e.availability_id
|
||||
end
|
||||
|
@ -1,5 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.array!(@events) do |event|
|
||||
json.partial! 'api/events/event', event: event
|
||||
json.event_image_medium event.event_image.attachment.medium.url if event.event_image
|
||||
|
||||
end
|
||||
|
@ -4,7 +4,6 @@ en:
|
||||
machine_form:
|
||||
name: "Name"
|
||||
illustration: "Visual"
|
||||
add_an_illustration: "Add a visual"
|
||||
description: "Description"
|
||||
technical_specifications: "Technical specifications"
|
||||
attached_files_pdf: "Attached files (pdf)"
|
||||
@ -17,7 +16,6 @@ en:
|
||||
training_form:
|
||||
name: "Name"
|
||||
illustration: "Illustration"
|
||||
add_an_illustration: "Add an illustration"
|
||||
description: "Description"
|
||||
add_a_new_training: "Add a new training"
|
||||
validate_your_training: "Validate your training"
|
||||
@ -33,7 +31,6 @@ en:
|
||||
space_form:
|
||||
name: "Name"
|
||||
illustration: "Illustration"
|
||||
add_an_illustration: "Add an illustration"
|
||||
description: "Description"
|
||||
characteristics: "Characteristics"
|
||||
attached_files_pdf: "Attached files (pdf)"
|
||||
@ -44,6 +41,52 @@ en:
|
||||
ACTION_space: "{ACTION, select, create{Create} other{Update}} the space"
|
||||
create_success: "The space was created successfully"
|
||||
update_success: "The space was updated successfully"
|
||||
event_form:
|
||||
title: "Title"
|
||||
matching_visual: "Matching visual"
|
||||
description: "Description"
|
||||
attachments: "Attachments"
|
||||
add_a_new_file: "Add a new file"
|
||||
event_category: "Event category"
|
||||
dates_and_opening_hours: "Dates and opening hours"
|
||||
all_day: "All day"
|
||||
all_day_help: "Will the event last all day or do you want to set times?"
|
||||
start_date: "Start date"
|
||||
end_date: "End date"
|
||||
start_time: "Start time"
|
||||
end_time: "End time"
|
||||
recurrence: "Recurrence"
|
||||
_and_ends_on: "and ends on"
|
||||
prices_and_availabilities: "Prices and availabilities"
|
||||
standard_rate: "Standard rate"
|
||||
0_equal_free: "0 = free"
|
||||
fare_class: "Fare class"
|
||||
price: "Price"
|
||||
seats_available: "Seats available"
|
||||
seats_help: "If you leave this field empty, this event will be available without reservations."
|
||||
event_themes: "Event themes"
|
||||
age_range: "Age range"
|
||||
add_price: "Add a price"
|
||||
ACTION_event: "{ACTION, select, create{Create} other{Update}} the event"
|
||||
create_success: "The event was created successfully"
|
||||
events_updated: "{COUNT, plural, =1{One event was} other{{COUNT} Events were}} successfully updated"
|
||||
events_not_updated: "{TOTAL, plural, =1{The event was} other{On {TOTAL} events {COUNT, plural, =1{one was} other{{COUNT} were}}}} not updated."
|
||||
error_deleting_reserved_price: "Unable to remove the requested price because it is associated with some existing reservations"
|
||||
other_error: "An unexpected error occurred while updating the event"
|
||||
recurring:
|
||||
none: "None"
|
||||
every_days: "Every days"
|
||||
every_week: "Every week"
|
||||
every_month: "Every month"
|
||||
every_year: "Every year"
|
||||
update_recurrent_modal:
|
||||
title: "Periodic event update"
|
||||
edit_recurring_event: "You're about to update a periodic event. What do you want to update?"
|
||||
edit_this_event: "Only this event"
|
||||
edit_this_and_next: "This event and the followings"
|
||||
edit_all: "All events"
|
||||
date_wont_change: "Warning: you have changed the event date. This modification won't be propagated to other occurrences of the periodic event."
|
||||
confirm: "Update the {MODE, select, single{event} other{events}}"
|
||||
#add a new machine
|
||||
machines_new:
|
||||
declare_a_new_machine: "Declare a new machine"
|
||||
|
Loading…
x
Reference in New Issue
Block a user