1
0
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:
Sylvain 2022-11-08 17:41:18 +01:00
parent 8df60a8712
commit 3d796549f2
29 changed files with 724 additions and 767 deletions

View 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;
}
}

View 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;
}
}

View 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;
}
}

View File

@ -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>> {

View 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;
}
}

View File

@ -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">

View 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']));

View File

@ -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']));

View File

@ -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>
);
};

View File

@ -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();
}

View File

@ -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;
}

View File

@ -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;
};
}

View File

@ -1,5 +0,0 @@
export interface EventTheme {
id: number,
name: string,
related_to: number
}

View File

@ -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
}>
}
}

View File

@ -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', {

View File

@ -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";

View 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;
}
}

View File

@ -0,0 +1,9 @@
.update-recurrent-modal {
label {
margin-left: 2rem;
span {
margin-left: 1rem;
}
}
}

View File

@ -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:&#xf03e;/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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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]">

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"