diff --git a/app/frontend/src/javascript/components/events/event-reservation-item.tsx b/app/frontend/src/javascript/components/events/event-reservation-item.tsx new file mode 100644 index 000000000..d9cd2ca4c --- /dev/null +++ b/app/frontend/src/javascript/components/events/event-reservation-item.tsx @@ -0,0 +1,102 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { react2angular } from 'react2angular'; +import { Reservation } from '../../models/reservation'; +import FormatLib from '../../lib/format'; +import { IApplication } from '../../models/application'; + +declare const Application: IApplication; + +interface EventReservationItemProps { + reservation: Reservation; +} + +/** + * event reservation item component + */ +export const EventReservationItem: React.FC<EventReservationItemProps> = ({ reservation }) => { + const { t } = useTranslation('logged'); + + /** + * Return the formatted localized date of the event + */ + const formatDate = (): string => { + return `${FormatLib.date(reservation.start_at)} ${FormatLib.time(reservation.start_at)} - ${FormatLib.time(reservation.end_at)}`; + }; + + /** + * Build the ticket for event price category user reservation + */ + const buildTicket = (ticket) => { + return ( + <> + <label>{t('app.logged.event_reservation_item.NUMBER_of_NAME_places_reserved', { NUMBER: ticket.booked, NAME: ticket.price_category.name })}</label> + {reservation.booking_users_attributes.filter(u => u.event_price_category_id === ticket.event_price_category_id).map(u => { + return ( + <p key={u.id} className='name'>{u.name}</p> + ); + })} + </> + ); + }; + + /** + * Return the pre-registration status + */ + const preRegistrationStatus = () => { + if (!reservation.validated_at && !reservation.canceled_at && !reservation.is_paid) { + return t('app.logged.event_reservation_item.in_the_process_of_validation'); + } else if (reservation.validated_at && !reservation.canceled_at && !reservation.is_paid) { + return t('app.logged.event_reservation_item.settle_your_payment'); + } else if (reservation.is_paid && !reservation.canceled_at) { + return t('app.logged.event_reservation_item.paid'); + } else if (reservation.canceled_at) { + return t('app.logged.event_reservation_item.paid'); + } + }; + + return ( + <div className="event-reservation-item"> + <div className="event-reservation-item__event"> + <div className="infos"> + <label>{t('app.logged.event_reservation_item.event')}</label> + <p>{reservation.event_title}</p> + <span className='date'>{formatDate()}</span> + </div> + <div className="types"> + {/* {reservation.event_type === 'family' && + <span className="">{t('app.logged.event_reservation_item.family')}</span> + } + {reservation.event_type === 'nominative' && + <span className="">{t('app.logged.event_reservation_item.nominative')}</span> + } */} + {reservation.event_pre_registration && + // eslint-disable-next-line fabmanager/no-bootstrap, fabmanager/no-utilities + <span className="badge text-xs bg-info">{t('app.logged.event_reservation_item.pre_registration')}</span> + } + </div> + </div> + <div className="event-reservation-item__reservation"> + <div className='list'> + <label>{t('app.logged.event_reservation_item.NUMBER_normal_places_reserved', { NUMBER: reservation.nb_reserve_places })}</label> + {reservation.booking_users_attributes.filter(u => !u.event_price_category_id).map(u => { + return ( + <p key={u.id} className='name'>{u.name}</p> + ); + })} + {reservation.tickets.map(ticket => { + return buildTicket(ticket); + })} + </div> + {reservation.event_pre_registration && + <div className='status'> + <label>{t('app.logged.event_reservation_item.tracking_your_reservation')}</label> + <p className="">{preRegistrationStatus()}</p> + </div> + } + </div> + </div> + ); +}; + +Application.Components.component('eventReservationItem', react2angular(EventReservationItem, ['reservation'])); diff --git a/app/frontend/src/javascript/models/reservation.ts b/app/frontend/src/javascript/models/reservation.ts index db950a40d..f5abfe7b9 100644 --- a/app/frontend/src/javascript/models/reservation.ts +++ b/app/frontend/src/javascript/models/reservation.ts @@ -30,7 +30,7 @@ export interface Reservation { name: string }, nb_reserve_places?: number, - tickets_attributes?: { + tickets_attributes?: Array<{ event_price_category_id: number, event_price_category?: { id: number, @@ -40,18 +40,38 @@ export interface Reservation { name: string } }, - booked: boolean, + booked: number, created_at?: TDateISO - }, + }>, + tickets?: Array<{ + event_price_category_id: number, + event_price_category?: { + id: number, + price_category_id: number, + price_category: { + id: number, + name: string + } + }, + booked: number, + created_at?: TDateISO + }>, total_booked_seats?: number, created_at?: TDateISO, - booking_users_attributes?: { + booking_users_attributes?: Array<{ id: number, name: string, event_price_category_id: number, booked_id: number, booked_type: string, - } + }>, + start_at: TDateISO, + end_at: TDateISO, + event_type?: string, + event_title?: string, + event_pre_registration?: boolean + validated_at?: TDateISO, + is_paid?: boolean, } export interface ReservationIndexFilter extends ApiFilter { diff --git a/app/frontend/src/stylesheets/application.scss b/app/frontend/src/stylesheets/application.scss index cf9ff38aa..2d793e9d2 100644 --- a/app/frontend/src/stylesheets/application.scss +++ b/app/frontend/src/stylesheets/application.scss @@ -47,11 +47,13 @@ @import "modules/dashboard/reservations/prepaid-packs-panel"; @import "modules/dashboard/reservations/reservations-dashboard"; @import "modules/dashboard/reservations/reservations-panel"; -@import "modules/events/event"; -@import "modules/events/events"; @import "modules/events/event-form"; +@import "modules/events/event-reservation"; +@import "modules/events/event"; +@import "modules/events/events-dashboard"; +@import "modules/events/events-settings"; +@import "modules/events/events"; @import "modules/events/update-recurrent-modal"; -@import "modules/events/events-settings.scss"; @import "modules/family-account/child-form"; @import "modules/family-account/child-item"; @import "modules/family-account/children-dashboard"; diff --git a/app/frontend/src/stylesheets/modules/events/event-reservation.scss b/app/frontend/src/stylesheets/modules/events/event-reservation.scss new file mode 100644 index 000000000..eacfb1ec2 --- /dev/null +++ b/app/frontend/src/stylesheets/modules/events/event-reservation.scss @@ -0,0 +1,60 @@ +.event-reservation { + display: flex; + flex-direction: column; + gap: 1.6rem; + + &-item { + padding: 1.6rem 1.6rem 0; + display: flex; + flex-direction: column; + background-color: var(--gray-soft-lightest); + border-radius: var(--border-radius); + + label { + margin: 0; + @include text-xs; + color: var(--gray-hard-light); + } + p { + margin: 0; + @include text-base(600); + } + .date { @include text-sm; } + + &__event { + padding-bottom: 1.2rem; + display: flex; + justify-content: space-between; + align-items: center; + gap: 1.6rem; + border-bottom: 1px solid var(--gray-soft-dark); + } + + &__reservation { + display: flex; + + & > div { + padding: 1.2rem 1.6rem 1.2rem 0; + flex: 1; + } + .list { + display: flex; + flex-direction: column; + row-gap: 0.5rem; + label:not(:first-of-type) { + margin-top: 1rem; + } + } + .name { @include text-sm(500); } + + .status { + padding-left: 1.6rem; + display: flex; + flex-direction: column; + justify-content: center; + border-left: 1px solid var(--gray-soft-dark); + } + } + + } +} \ No newline at end of file diff --git a/app/frontend/src/stylesheets/modules/events/events-dashboard.scss b/app/frontend/src/stylesheets/modules/events/events-dashboard.scss new file mode 100644 index 000000000..7c6c70bc9 --- /dev/null +++ b/app/frontend/src/stylesheets/modules/events/events-dashboard.scss @@ -0,0 +1,5 @@ +.events-dashboard { + max-width: 1600px; + margin: 0 auto; + padding-bottom: 6rem; +} \ No newline at end of file diff --git a/app/frontend/templates/admin/members/edit.html b/app/frontend/templates/admin/members/edit.html index 752bffe3a..287a61911 100644 --- a/app/frontend/templates/admin/members/edit.html +++ b/app/frontend/templates/admin/members/edit.html @@ -207,29 +207,11 @@ <h4 class="text-u-c"><i class="fa fa-tag m-r-xs"></i> {{ 'app.admin.members_edit.next_events' | translate }}</h4> </div> <div class="widget-content bg-light wrapper r-b"> - <ul class="list-unstyled" ng-if="user.events_reservations.length > 0"> - <li ng-repeat="r in user.events_reservations | eventsReservationsFilter:'future'" class="m-b"> - <a class="font-sbold" ui-sref="app.public.events_show({id: r.reservable.id})">{{r.reservable.title}}</a> - <span class="label label-warning wrapper-sm">{{ r.start_at | amDateFormat:'LLL' }} - {{ r.end_at | amDateFormat:'LT' }}</span> - <span ng-if="r.nb_reserve_places > 0"> - <br/> - <span translate translate-values="{ NUMBER: r.nb_reserve_places }">{{ 'app.admin.members_edit.NUMBER_full_price_tickets_reserved' }}</span> - <span ng-repeat="bu in r.booking_users_attributes | filter:{event_price_category_id:null}"> - <br/> - <span ng-if="bu.booked_type !== 'User'">{{bu.name}}</span> - <a ui-sref="app.logged.members_show({id: bu.booked_id})" ng-if="bu.booked_type === 'User'">{{bu.name}}</a> - </span> - </span> - <span ng-repeat="ticket in r.tickets"> - <br/> - <span translate translate-values="{ NUMBER: ticket.booked, NAME: ticket.price_category.name }">{{ 'app.admin.members_edit.NUMBER_NAME_tickets_reserved' }}</span> - <span ng-repeat="bu in r.booking_users_attributes | filter:{event_price_category_id:ticket.event_price_category_id}"> - <br/> - <span ng-if="bu.booked_type !== 'User'">{{bu.name}}</span> - <a ui-sref="app.logged.members_show({id: bu.booked_id})" ng-if="bu.booked_type === 'User'">{{bu.name}}</a> - </span> - </span> - </li> - </ul> + <div class="list-unstyled" ng-if="user.events_reservations.length > 0"> + <div ng-repeat="r in user.events_reservations | eventsReservationsFilter:'future'" class="m-b"> + <event-reservation-item reservation="r"></event-reservation-item> + </div> + </div> <div ng-if="(user.events_reservations | eventsReservationsFilter:'future').length == 0" translate>{{ 'app.admin.members_edit.no_upcoming_events' }}</div> </div> </div> @@ -240,11 +222,11 @@ <h4 class="text-u-c"><i class="fa fa-tag m-r-xs"></i> {{ 'app.admin.members_edit.passed_events' | translate }}</h4> </div> <div class="widget-content bg-light auto wrapper r-b"> - <ul class="list-unstyled" ng-if="user.events_reservations.length > 0"> - <li ng-repeat="r in user.events_reservations | eventsReservationsFilter:'passed'" class="m-b"> - <span class="font-sbold">{{r.reservable.title}}</span> - <span class="label label-info text-white wrapper-sm">{{ r.start_at | amDateFormat:'LLL' }} - {{ r.end_at | amDateFormat:'LT' }}</span> - </li> - </ul> + <div class="list-unstyled" ng-if="user.events_reservations.length > 0"> + <div ng-repeat="r in user.events_reservations | eventsReservationsFilter:'passed'" class="m-b"> + <event-reservation-item reservation="r"></event-reservation-item> + </div> + </div> <div ng-if="(user.events_reservations | eventsReservationsFilter:'passed').length == 0" translate>{{ 'app.admin.members_edit.no_passed_events' }}</div> </div> </div> diff --git a/app/frontend/templates/dashboard/events.html b/app/frontend/templates/dashboard/events.html index 9af91bd8f..f9333495d 100644 --- a/app/frontend/templates/dashboard/events.html +++ b/app/frontend/templates/dashboard/events.html @@ -8,7 +8,7 @@ </section> - <div class="row no-gutter"> + <div class="row events-dashboard"> <div class="col-md-6"> <div class="widget panel b-a m m-t-lg"> @@ -16,33 +16,11 @@ <h4 class="text-u-c"><i class="fa fa-tag m-r-xs"></i> {{ 'app.logged.dashboard.events.your_next_events' | translate }}</h4> </div> <div class="widget-content bg-light wrapper r-b"> - <ul class="list-unstyled" ng-if="user.events_reservations.length > 0"> - <li ng-repeat="r in user.events_reservations | eventsReservationsFilter:'future'" class="m-b"> - <a class="font-sbold" ui-sref="app.public.events_show({id: r.reservable.id})">{{r.reservable.title}}</a> - - - <span class="label label-warning wrapper-sm">{{ r.start_at | amDateFormat:'LLL' }} - {{ r.end_at | amDateFormat:'LT' }}</span> - <br/> - <span translate - translate-values="{NUMBER: r.nb_reserve_places}"> - {{ 'app.logged.dashboard.events.NUMBER_normal_places_reserved' }} - </span> - <span ng-repeat="bu in r.booking_users_attributes | filter:{event_price_category_id:null}"> - <br/> - <span>{{bu.name}}</span> - </span> - <span ng-repeat="ticket in r.tickets"> - <br/> - <span translate - translate-values="{NUMBER: ticket.booked, NAME: ticket.price_category.name}"> - {{ 'app.logged.dashboard.events.NUMBER_of_NAME_places_reserved' }} - </span> - <span ng-repeat="bu in r.booking_users_attributes | filter:{event_price_category_id:ticket.event_price_category_id}"> - <br/> - <span>{{bu.name}}</span> - </span> - </span> - </li> - </ul> + <div class="list-unstyled event-reservation" ng-if="user.events_reservations.length > 0"> + <div ng-repeat="r in user.events_reservations | eventsReservationsFilter:'future'"> + <event-reservation-item reservation="r"></event-reservation-item> + </div> + </div> <div ng-if="(user.events_reservations | eventsReservationsFilter:'future').length == 0" translate>{{ 'app.logged.dashboard.events.no_events_to_come' }}</div> </div> </div> @@ -53,11 +31,11 @@ <h4 class="text-u-c"><i class="fa fa-tag m-r-xs"></i> {{ 'app.logged.dashboard.events.your_previous_events' | translate }}</h4> </div> <div class="widget-content bg-light auto wrapper r-b"> - <ul class="list-unstyled" ng-if="user.events_reservations.length > 0"> - <li ng-repeat="r in user.events_reservations | eventsReservationsFilter:'passed'" class="m-b"> - <span class="font-sbold">{{r.reservable.title}}</span> - <span class="label label-info text-white wrapper-sm">{{ r.start_at | amDateFormat:'LLL' }} - {{ r.end_at | amDateFormat:'LT' }}</span> - </li> - </ul> + <div class="list-unstyled" ng-if="user.events_reservations.length > 0"> + <div ng-repeat="r in user.events_reservations | eventsReservationsFilter:'passed'" class="m-b"> + <event-reservation-item reservation="r"></event-reservation-item> + </div> + </div> <div ng-if="(user.events_reservations | eventsReservationsFilter:'passed').length == 0" translate>{{ 'app.logged.dashboard.events.no_passed_events' }}</div> </div> </div> diff --git a/app/views/api/members/show.json.jbuilder b/app/views/api/members/show.json.jbuilder index 051b19718..28bdadf3f 100644 --- a/app/views/api/members/show.json.jbuilder +++ b/app/views/api/members/show.json.jbuilder @@ -85,6 +85,10 @@ json.events_reservations @member.reservations.where(reservable_type: 'Event').jo json.reservable sr.reservation.reservable json.reservable_type 'Event' json.event_type sr.reservation.reservable.event_type + json.event_title sr.reservation.reservable.title + json.event_pre_registration sr.reservation.reservable.pre_registration + json.validated_at sr.validated_at + json.is_paid sr.reservation.invoice_items.count.positive? json.canceled_at sr.canceled_at json.booking_users_attributes sr.reservation.booking_users.order(booked_type: :desc) do |bu| json.id bu.id diff --git a/config/locales/app.admin.en.yml b/config/locales/app.admin.en.yml index 670227373..c456b3065 100644 --- a/config/locales/app.admin.en.yml +++ b/config/locales/app.admin.en.yml @@ -158,7 +158,7 @@ en: family: "Event family" pre_registration: "Pre-registration" pre_registration_help: "If this option is checked, administrators and managers must validate registrations before they become final." - pre_registration_end_date: "Pre-registration end date" + pre_registration_end_date: "Deadline for pre-registration" plan_form: ACTION_title: "{ACTION, select, create{New} other{Update the}} plan" tab_settings: "Settings" diff --git a/config/locales/app.admin.fr.yml b/config/locales/app.admin.fr.yml index a1e0d39b7..8a5768272 100644 --- a/config/locales/app.admin.fr.yml +++ b/config/locales/app.admin.fr.yml @@ -158,7 +158,7 @@ fr: family: "Evénement famille" pre_registration: "Pré-inscription" pre_registration_help: "Si cette option est cochée, les administrateurs et les gestionnaires devent valider les inscriptions avant qu'elles ne soient définitives." - pre_registration_end_date: "Date de fin de pré-inscription" + pre_registration_end_date: "Date limite de pré-inscription" plan_form: ACTION_title: "{ACTION, select, create{Nouvelle} other{Mettre à jour la}} formule d'abonnement" tab_settings: "Paramètres" diff --git a/config/locales/app.logged.en.yml b/config/locales/app.logged.en.yml index f284fbc5d..8698bc923 100644 --- a/config/locales/app.logged.en.yml +++ b/config/locales/app.logged.en.yml @@ -37,6 +37,18 @@ en: edit_profile: "Change my data" after_edition_info_html: "Once your data are up to date, <strong>click on the synchronization button below</strong>, or <strong>disconnect then reconnect</strong> for your changes to take effect." sync_profile: "Sync my profile" + event_reservation_item: + event: "Event" + family: "Family" + nominative: "Nominative" + pre_registration: "Pre-registration" + NUMBER_normal_places_reserved: "{NUMBER} {NUMBER, plural, =0{} =1{normal place reserved} other{normal places reserved}}" + NUMBER_of_NAME_places_reserved: "{NUMBER} {NUMBER, plural, =0{} =1{of {NAME} place reserved} other{of {NAME} places reserved}}" + tracking_your_reservation: "Tracking your reservation" + in_the_process_of_validation: "In the process of validation" + settle_your_payment: "Come to the reception desk to settle" + paid: "Paid" + canceled: "Canceled" dashboard: #dashboard: public profile profile: diff --git a/config/locales/app.logged.fr.yml b/config/locales/app.logged.fr.yml index 24550f9d1..f9a41a3e9 100644 --- a/config/locales/app.logged.fr.yml +++ b/config/locales/app.logged.fr.yml @@ -37,6 +37,18 @@ fr: edit_profile: "Modifier mes données" after_edition_info_html: "Une fois que vos données sont à jour, <strong>cliquez sur le bouton de synchronisation ci-dessous</strong>, ou <strong>déconnectez-vous puis reconnectez-vous</strong> pour que vos changements prennent effet." sync_profile: "Synchroniser mon profil" + event_reservation_item: + event: 'Événement' + family: 'Famille' + nominative: 'Nominatif' + pre_registration: 'Pré-inscription' + NUMBER_normal_places_reserved: "{NUMBER} {NUMBER, plural, =0{} =1{place normale réservée} other{places normales réservées}}" + NUMBER_of_NAME_places_reserved: "{NUMBER} {NUMBER, plural, =0{} =1{place {NAME} réservée} other{places {NAME} réservées}}" + tracking_your_reservation: "Suivi de votre réservation" + in_the_process_of_validation: "En cours de validation" + settle_your_payment: "Venez régler à l'accueil" + paid: "Payé" + canceled: "Annulé" dashboard: #dashboard: public profile profile: