1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2024-11-29 10:24:20 +01:00

(feat) show reservations panel

This commit is contained in:
Sylvain 2022-06-28 18:42:41 +02:00
parent ea1883e406
commit 9b601bc438
16 changed files with 194 additions and 73 deletions

View File

@ -4,12 +4,14 @@ interface FabPopoverProps {
title: string, title: string,
className?: string, className?: string,
headerButton?: ReactNode, headerButton?: ReactNode,
position?: 'bottom' | 'right' | 'left'
} }
/** /**
* This component is a template for a popovers (bottom) that wraps the application style * This component is a template for a popovers (bottom) that wraps the application style.
* Please note that the parent element must be set `position: relative;` otherwise the popover won't be placed correctly.
*/ */
export const FabPopover: React.FC<FabPopoverProps> = ({ title, className, headerButton, children }) => { export const FabPopover: React.FC<FabPopoverProps> = ({ title, className, headerButton, position = 'bottom', children }) => {
/** /**
* Check if the header button should be present * Check if the header button should be present
*/ */
@ -18,7 +20,7 @@ export const FabPopover: React.FC<FabPopoverProps> = ({ title, className, header
}; };
return ( return (
<div className={`fab-popover ${className || ''}`}> <div className={`fab-popover fab-popover__${position} ${className || ''}`}>
<div className="popover-title"> <div className="popover-title">
<h3>{title}</h3> <h3>{title}</h3>
{hasHeaderButton() && headerButton} {hasHeaderButton() && headerButton}

View File

@ -1,28 +0,0 @@
import React, { useEffect, useState } from 'react';
import { FabPanel } from '../../base/fab-panel';
import { Reservation } from '../../../models/reservation';
import ReservationAPI from '../../../api/reservation';
interface MachineReservationsProps {
userId: number,
onError: (message: string) => void,
}
/**
* List all machine reservations for the given user
*/
export const MachineReservations: React.FC<MachineReservationsProps> = ({ userId, onError }) => {
const [reservations, setReservations] = useState<Array<Reservation>>([]);
useEffect(() => {
ReservationAPI.index({ user_id: userId, reservable_type: 'Machine' })
.then(res => setReservations(res))
.catch(error => onError(error));
}, []);
return (
<FabPanel className="machine-reservations">
{reservations.map(r => JSON.stringify(r))}
</FabPanel>
);
};

View File

@ -1,8 +1,9 @@
import React from 'react'; import React, { useEffect, useState } from 'react';
import { IApplication } from '../../../models/application'; import { IApplication } from '../../../models/application';
import { react2angular } from 'react2angular'; import { react2angular } from 'react2angular';
import { MachineReservations } from './machine-reservations'; import { ReservationsPanel } from './reservations-panel';
import { SpaceReservations } from './space-reservations'; import SettingAPI from '../../../api/setting';
import { SettingName } from '../../../models/setting';
declare const Application: IApplication; declare const Application: IApplication;
@ -15,10 +16,18 @@ interface ReservationsDashboardProps {
* User dashboard showing everything about his spaces/machine reservations and also remaining credits * User dashboard showing everything about his spaces/machine reservations and also remaining credits
*/ */
const ReservationsDashboard: React.FC<ReservationsDashboardProps> = ({ onError, userId }) => { const ReservationsDashboard: React.FC<ReservationsDashboardProps> = ({ onError, userId }) => {
const [modules, setModules] = useState<Map<SettingName, string>>();
useEffect(() => {
SettingAPI.query([SettingName.SpacesModule, SettingName.MachinesModule])
.then(res => setModules(res))
.catch(error => onError(error));
}, []);
return ( return (
<div className="reservations-dashboard"> <div className="reservations-dashboard">
<MachineReservations userId={userId} onError={onError} /> {modules?.get(SettingName.MachinesModule) !== 'false' && <ReservationsPanel userId={userId} onError={onError} reservableType="Machine" />}
<SpaceReservations userId={userId} onError={onError} /> {modules?.get(SettingName.SpacesModule) !== 'false' && <ReservationsPanel userId={userId} onError={onError} reservableType="Space" />}
</div> </div>
); );
}; };

View File

@ -0,0 +1,107 @@
import React, { ReactNode, useEffect, useState } from 'react';
import { FabPanel } from '../../base/fab-panel';
import { Reservation } from '../../../models/reservation';
import ReservationAPI from '../../../api/reservation';
import { useTranslation } from 'react-i18next';
import moment from 'moment';
import { Loader } from '../../base/loader';
import FormatLib from '../../../lib/format';
import { FabPopover } from '../../base/fab-popover';
import { useImmer } from 'use-immer';
interface SpaceReservationsProps {
userId: number,
onError: (message: string) => void,
reservableType: 'Machine' | 'Space'
}
/**
* List all reservations for the given user and the given type
*/
const ReservationsPanel: React.FC<SpaceReservationsProps> = ({ userId, onError, reservableType }) => {
const { t } = useTranslation('logged');
const [reservations, setReservations] = useState<Array<Reservation>>([]);
const [details, updateDetails] = useImmer<Record<number, boolean>>({});
useEffect(() => {
ReservationAPI.index({ user_id: userId, reservable_type: reservableType })
.then(res => setReservations(res))
.catch(error => onError(error));
}, []);
/**
* Return the reservations for the given period
*/
const reservationsByDate = (state: 'passed' | 'futur'): Array<Reservation> => {
return reservations.filter(r => {
return !!r.slots_attributes.find(s => {
return (state === 'passed' && moment(s.start_at).isBefore()) ||
(state === 'futur' && moment(s.start_at).isAfter());
});
});
};
/**
* Panel title
*/
const header = (): ReactNode => {
return (
<div>
{t(`app.logged.dashboard.reservations.reservations_panel.title_${reservableType}`)}
</div>
);
};
/**
* Show/hide the slots details for the given reservation
*/
const toggleDetails = (reservationId: number): () => void => {
return () => {
updateDetails(draft => {
draft[reservationId] = !draft[reservationId];
});
};
};
/**
* Render the reservation in a user-friendly way
*/
const renderReservation = (reservation: Reservation): ReactNode => {
return (
<li key={reservation.id} className="reservation">
<a className={`reservation-title ${details[reservation.id] ? 'clicked' : ''}`} onClick={toggleDetails(reservation.id)}>{reservation.reservable.name}</a>
{details[reservation.id] && <FabPopover title={t('app.logged.dashboard.reservations.reservations_panel.slots_details')}>
{reservation.slots_attributes.map(
(slot) => <span key={slot.id} className="slot-details">
{FormatLib.date(slot.start_at)}, {FormatLib.time(slot.start_at)} - {FormatLib.time(slot.end_at)}
</span>
)}
</FabPopover>}
</li>
);
};
return (
<FabPanel className="reservations-panel" header={header()}>
<h4>{t('app.logged.dashboard.reservations.reservations_panel.upcoming')}</h4>
<ul>
{reservationsByDate('futur').map(r => renderReservation(r))}
</ul>
<h4>{t('app.logged.dashboard.reservations.reservations_panel.passed')}</h4>
<ul>
{reservationsByDate('passed').map(r => renderReservation(r))}
</ul>
</FabPanel>
);
};
const ReservationsPanelWrapper: React.FC<SpaceReservationsProps> = (props) => {
return (
<Loader>
<ReservationsPanel {...props} />
</Loader>
);
};
export { ReservationsPanelWrapper as ReservationsPanel };

View File

@ -1,28 +0,0 @@
import React, { useEffect, useState } from 'react';
import { FabPanel } from '../../base/fab-panel';
import { Reservation } from '../../../models/reservation';
import ReservationAPI from '../../../api/reservation';
interface SpaceReservationsProps {
userId: number,
onError: (message: string) => void,
}
/**
* List all space reservations for the given user
*/
export const SpaceReservations: React.FC<SpaceReservationsProps> = ({ userId, onError }) => {
const [reservations, setReservations] = useState<Array<Reservation>>([]);
useEffect(() => {
ReservationAPI.index({ user_id: userId, reservable_type: 'Space' })
.then(res => setReservations(res))
.catch(error => onError(error));
}, []);
return (
<FabPanel className="space-reservations">
{reservations.map(r => JSON.stringify(r))}
</FabPanel>
);
};

View File

@ -68,7 +68,7 @@ export const ConfigurePacksButton: React.FC<ConfigurePacksButtonProps> = ({ pack
<button className="packs-button" onClick={toggleShowList}> <button className="packs-button" onClick={toggleShowList}>
<i className="fas fa-box" /> <i className="fas fa-box" />
</button> </button>
{showList && <FabPopover title={t('app.admin.configure_packs_button.packs')} headerButton={renderAddButton()} className="fab-popover__right"> {showList && <FabPopover title={t('app.admin.configure_packs_button.packs')} headerButton={renderAddButton()} position="right">
<ul> <ul>
{packs?.map(p => {packs?.map(p =>
<li key={p.id} className={p.disabled ? 'disabled' : ''}> <li key={p.id} className={p.disabled ? 'disabled' : ''}>

View File

@ -68,7 +68,7 @@ export const ConfigureExtendedPricesButton: React.FC<ConfigureExtendedPricesButt
<button className="extended-prices-button" onClick={toggleShowList}> <button className="extended-prices-button" onClick={toggleShowList}>
<i className="fas fa-stopwatch" /> <i className="fas fa-stopwatch" />
</button> </button>
{showList && <FabPopover title={t('app.admin.configure_extended_prices_button.extended_prices')} headerButton={renderAddButton()} className="fab-popover__right"> {showList && <FabPopover title={t('app.admin.configure_extended_prices_button.extended_prices')} headerButton={renderAddButton()} position="right">
<ul> <ul>
{extendedPrices?.map(extendedPrice => {extendedPrices?.map(extendedPrice =>
<li key={extendedPrice.id}> <li key={extendedPrice.id}>

View File

@ -14,7 +14,7 @@ export type ruleTypes = {
/** /**
* `error` and `warning` props can be manually set. * `error` and `warning` props can be manually set.
* Automatic error handling is done through the `formState` prop. * Automatic error handling is done through the `formState` prop.
* Even for manual error/warning, the `formState` prop is required, because it is used to determine is the field is dirty. * Even for manual error/warning, the `formState` prop is required, because it is used to determine if the field is dirty.
*/ */
export interface AbstractFormComponent<TFieldValues> { export interface AbstractFormComponent<TFieldValues> {
error?: { message: string }, error?: { message: string },

View File

@ -21,10 +21,23 @@ export interface Reservation {
reservable_id: number, reservable_id: number,
reservable_type: ReservableType, reservable_type: ReservableType,
slots_attributes: Array<ReservationSlot>, slots_attributes: Array<ReservationSlot>,
reservable?: {
id: number,
name: string
},
nb_reserve_places?: number, nb_reserve_places?: number,
tickets_attributes?: { tickets_attributes?: {
event_price_category_id: number, event_price_category_id: number,
event_price_category?: {
id: number,
price_category_id: number,
price_category: {
id: number,
name: string
}
},
booked: boolean, booked: boolean,
created_at?: TDateISO
}, },
total_booked_seats?: number, total_booked_seats?: number,
created_at?: TDateISO, created_at?: TDateISO,

View File

@ -30,6 +30,7 @@
@import "modules/base/fab-text-editor"; @import "modules/base/fab-text-editor";
@import "modules/base/labelled-input"; @import "modules/base/labelled-input";
@import "modules/calendar/calendar"; @import "modules/calendar/calendar";
@import "modules/dashboard/reservations/reservations-panel";
@import "modules/events/event"; @import "modules/events/event";
@import "modules/form/abstract-form-item"; @import "modules/form/abstract-form-item";
@import "modules/form/form-input"; @import "modules/form/form-input";

View File

@ -0,0 +1,37 @@
.reservations-panel {
.reservation {
position: relative;
&-title {
cursor: pointer;
&.clicked {
color: var(--secondary-dark);
}
}
.slot-details {
display: block;
}
.fab-popover {
left: 0;
}
}
}
@media (min-width: 1460px) {
.reservations-panel {
width: 45%;
}
}
@media (max-width: 1459px) {
.reservations-panel {
width: 95%;
}
}
@media (max-width: 992px) {
.reservations-panel {
width: 90%;
}
}

View File

@ -34,7 +34,7 @@
<td>{{ reservation.created_at | amDateFormat:'LL LTS' }}</td> <td>{{ reservation.created_at | amDateFormat:'LL LTS' }}</td>
<td> <td>
<span ng-if="reservation.nb_reserve_places > 0">{{ 'app.admin.event_reservations.full_price_' | translate }} {{reservation.nb_reserve_places}}<br/></span> <span ng-if="reservation.nb_reserve_places > 0">{{ 'app.admin.event_reservations.full_price_' | translate }} {{reservation.nb_reserve_places}}<br/></span>
<span ng-repeat="ticket in reservation.tickets">{{ticket.event_price_category.price_category.name}} : {{ticket.booked}}</span> <span ng-repeat="ticket in reservation.tickets_attributes">{{ticket.event_price_category.price_category.name}} : {{ticket.booked}}</span>
<div ng-show="isCancelled(reservation)" class="canceled-marker" translate>{{ 'app.admin.event_reservations.canceled' }}</div> <div ng-show="isCancelled(reservation)" class="canceled-marker" translate>{{ 'app.admin.event_reservations.canceled' }}</div>
</td> </td>
<td> <td>

View File

@ -44,6 +44,7 @@
<li ng-if="!isAuthorized(['admin', 'manager']) && hasProofOfIdentityTypes"><a ui-sref="app.logged.dashboard.proof_of_identity_files" translate>{{ 'app.public.common.my_supporting_documents_files' }}</a></li> <li ng-if="!isAuthorized(['admin', 'manager']) && hasProofOfIdentityTypes"><a ui-sref="app.logged.dashboard.proof_of_identity_files" translate>{{ 'app.public.common.my_supporting_documents_files' }}</a></li>
<li><a ui-sref="app.logged.dashboard.projects" translate>{{ 'app.public.common.my_projects' }}</a></li> <li><a ui-sref="app.logged.dashboard.projects" translate>{{ 'app.public.common.my_projects' }}</a></li>
<li><a ui-sref="app.logged.dashboard.trainings" translate>{{ 'app.public.common.my_trainings' }}</a></li> <li><a ui-sref="app.logged.dashboard.trainings" translate>{{ 'app.public.common.my_trainings' }}</a></li>
<li><a ui-sref="app.logged.dashboard.reservations" translate>{{ 'app.public.common.my_reservations' }}</a></li>
<li><a ui-sref="app.logged.dashboard.events" translate>{{ 'app.public.common.my_events' }}</a></li> <li><a ui-sref="app.logged.dashboard.events" translate>{{ 'app.public.common.my_events' }}</a></li>
<li><a ui-sref="app.logged.dashboard.invoices" ng-show="$root.modules.invoicing" translate>{{ 'app.public.common.my_invoices' }}</a></li> <li><a ui-sref="app.logged.dashboard.invoices" ng-show="$root.modules.invoicing" translate>{{ 'app.public.common.my_invoices' }}</a></li>
<li><a ui-sref="app.logged.dashboard.payment_schedules" ng-show="$root.modules.invoicing" translate>{{ 'app.public.common.my_payment_schedules' }}</a></li> <li><a ui-sref="app.logged.dashboard.payment_schedules" ng-show="$root.modules.invoicing" translate>{{ 'app.public.common.my_payment_schedules' }}</a></li>

View File

@ -12,7 +12,7 @@ json.slots_attributes reservation.slots do |s|
json.is_reserved true json.is_reserved true
end end
json.nb_reserve_places reservation.nb_reserve_places json.nb_reserve_places reservation.nb_reserve_places
json.tickets reservation.tickets do |t| json.tickets_attributes reservation.tickets do |t|
json.extract! t, :booked, :created_at json.extract! t, :booked, :created_at
json.event_price_category do json.event_price_category do
json.extract! t.event_price_category, :id, :price_category_id json.extract! t.event_price_category, :id, :price_category_id
@ -25,3 +25,7 @@ json.total_booked_seats reservation.total_booked_seats(canceled: true)
json.created_at reservation.created_at.iso8601 json.created_at reservation.created_at.iso8601
json.reservable_id reservation.reservable_id json.reservable_id reservation.reservable_id
json.reservable_type reservation.reservable_type json.reservable_type reservation.reservable_type
json.reservable do
json.id reservation.reservable.id
json.name reservation.reservable.name
end

View File

@ -16,7 +16,3 @@ json.user do
json.hours_used mc.users_credits.find_by(user_id: @reservation.statistic_profile.user_id).hours_used json.hours_used mc.users_credits.find_by(user_id: @reservation.statistic_profile.user_id).hours_used
end end
end end
json.reservable do
json.id @reservation.reservable.id
json.name @reservation.reservable.name
end

View File

@ -142,6 +142,13 @@ en:
save: "Save" save: "Save"
browse: "Browse" browse: "Browse"
edit: "Edit" edit: "Edit"
reservations:
reservations_panel:
title_Space: "My space reservations"
title_Machine: "My machines reservations"
upcoming: "Upcoming"
passed: "Passed"
slots_details: "Slots details"
#public profil of a member #public profil of a member
members_show: members_show:
members_list: "Members list" members_list: "Members list"