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:
parent
ea1883e406
commit
9b601bc438
@ -4,12 +4,14 @@ interface FabPopoverProps {
|
||||
title: string,
|
||||
className?: string,
|
||||
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
|
||||
*/
|
||||
@ -18,7 +20,7 @@ export const FabPopover: React.FC<FabPopoverProps> = ({ title, className, header
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`fab-popover ${className || ''}`}>
|
||||
<div className={`fab-popover fab-popover__${position} ${className || ''}`}>
|
||||
<div className="popover-title">
|
||||
<h3>{title}</h3>
|
||||
{hasHeaderButton() && headerButton}
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -1,8 +1,9 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { IApplication } from '../../../models/application';
|
||||
import { react2angular } from 'react2angular';
|
||||
import { MachineReservations } from './machine-reservations';
|
||||
import { SpaceReservations } from './space-reservations';
|
||||
import { ReservationsPanel } from './reservations-panel';
|
||||
import SettingAPI from '../../../api/setting';
|
||||
import { SettingName } from '../../../models/setting';
|
||||
|
||||
declare const Application: IApplication;
|
||||
|
||||
@ -15,10 +16,18 @@ interface ReservationsDashboardProps {
|
||||
* User dashboard showing everything about his spaces/machine reservations and also remaining credits
|
||||
*/
|
||||
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 (
|
||||
<div className="reservations-dashboard">
|
||||
<MachineReservations userId={userId} onError={onError} />
|
||||
<SpaceReservations userId={userId} onError={onError} />
|
||||
{modules?.get(SettingName.MachinesModule) !== 'false' && <ReservationsPanel userId={userId} onError={onError} reservableType="Machine" />}
|
||||
{modules?.get(SettingName.SpacesModule) !== 'false' && <ReservationsPanel userId={userId} onError={onError} reservableType="Space" />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -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 };
|
@ -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>
|
||||
);
|
||||
};
|
@ -68,7 +68,7 @@ export const ConfigurePacksButton: React.FC<ConfigurePacksButtonProps> = ({ pack
|
||||
<button className="packs-button" onClick={toggleShowList}>
|
||||
<i className="fas fa-box" />
|
||||
</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>
|
||||
{packs?.map(p =>
|
||||
<li key={p.id} className={p.disabled ? 'disabled' : ''}>
|
||||
|
@ -68,7 +68,7 @@ export const ConfigureExtendedPricesButton: React.FC<ConfigureExtendedPricesButt
|
||||
<button className="extended-prices-button" onClick={toggleShowList}>
|
||||
<i className="fas fa-stopwatch" />
|
||||
</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>
|
||||
{extendedPrices?.map(extendedPrice =>
|
||||
<li key={extendedPrice.id}>
|
||||
|
@ -14,7 +14,7 @@ export type ruleTypes = {
|
||||
/**
|
||||
* `error` and `warning` props can be manually set.
|
||||
* 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> {
|
||||
error?: { message: string },
|
||||
|
@ -21,10 +21,23 @@ export interface Reservation {
|
||||
reservable_id: number,
|
||||
reservable_type: ReservableType,
|
||||
slots_attributes: Array<ReservationSlot>,
|
||||
reservable?: {
|
||||
id: number,
|
||||
name: string
|
||||
},
|
||||
nb_reserve_places?: number,
|
||||
tickets_attributes?: {
|
||||
event_price_category_id: number,
|
||||
event_price_category?: {
|
||||
id: number,
|
||||
price_category_id: number,
|
||||
price_category: {
|
||||
id: number,
|
||||
name: string
|
||||
}
|
||||
},
|
||||
booked: boolean,
|
||||
created_at?: TDateISO
|
||||
},
|
||||
total_booked_seats?: number,
|
||||
created_at?: TDateISO,
|
||||
|
@ -30,6 +30,7 @@
|
||||
@import "modules/base/fab-text-editor";
|
||||
@import "modules/base/labelled-input";
|
||||
@import "modules/calendar/calendar";
|
||||
@import "modules/dashboard/reservations/reservations-panel";
|
||||
@import "modules/events/event";
|
||||
@import "modules/form/abstract-form-item";
|
||||
@import "modules/form/form-input";
|
||||
|
@ -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%;
|
||||
}
|
||||
}
|
@ -34,7 +34,7 @@
|
||||
<td>{{ reservation.created_at | amDateFormat:'LL LTS' }}</td>
|
||||
<td>
|
||||
<span ng-if="reservation.nb_reserve_places > 0">{{ 'app.admin.event_reservations.full_price_' | translate }} {{reservation.nb_reserve_places}}<br/></span>
|
||||
<span ng-repeat="ticket in reservation.tickets">{{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>
|
||||
</td>
|
||||
<td>
|
||||
|
@ -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><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.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.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>
|
||||
|
@ -12,7 +12,7 @@ json.slots_attributes reservation.slots do |s|
|
||||
json.is_reserved true
|
||||
end
|
||||
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.event_price_category do
|
||||
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.reservable_id reservation.reservable_id
|
||||
json.reservable_type reservation.reservable_type
|
||||
json.reservable do
|
||||
json.id reservation.reservable.id
|
||||
json.name reservation.reservable.name
|
||||
end
|
||||
|
@ -16,7 +16,3 @@ json.user do
|
||||
json.hours_used mc.users_credits.find_by(user_id: @reservation.statistic_profile.user_id).hours_used
|
||||
end
|
||||
end
|
||||
json.reservable do
|
||||
json.id @reservation.reservable.id
|
||||
json.name @reservation.reservable.name
|
||||
end
|
||||
|
@ -142,6 +142,13 @@ en:
|
||||
save: "Save"
|
||||
browse: "Browse"
|
||||
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
|
||||
members_show:
|
||||
members_list: "Members list"
|
||||
|
Loading…
Reference in New Issue
Block a user