forked from vergnet/application-amicale
441 lines
14 KiB
JavaScript
441 lines
14 KiB
JavaScript
// @flow
|
|
|
|
import * as React from 'react';
|
|
import {
|
|
Button,
|
|
Caption,
|
|
Card,
|
|
Headline,
|
|
Subheading,
|
|
withTheme,
|
|
} from 'react-native-paper';
|
|
import {StackNavigationProp} from '@react-navigation/stack';
|
|
import {BackHandler, View} from 'react-native';
|
|
import * as Animatable from 'react-native-animatable';
|
|
import i18n from 'i18n-js';
|
|
import {CalendarList} from 'react-native-calendars';
|
|
import type {DeviceType} from './EquipmentListScreen';
|
|
import type {CustomThemeType} from '../../../managers/ThemeManager';
|
|
import LoadingConfirmDialog from '../../../components/Dialogs/LoadingConfirmDialog';
|
|
import ErrorDialog from '../../../components/Dialogs/ErrorDialog';
|
|
import {
|
|
generateMarkedDates,
|
|
getFirstEquipmentAvailability,
|
|
getISODate,
|
|
getRelativeDateString,
|
|
getValidRange,
|
|
isEquipmentAvailable,
|
|
} from '../../../utils/EquipmentBooking';
|
|
import ConnectionManager from '../../../managers/ConnectionManager';
|
|
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
|
|
|
|
type PropsType = {
|
|
navigation: StackNavigationProp,
|
|
route: {
|
|
params?: {
|
|
item?: DeviceType,
|
|
},
|
|
},
|
|
theme: CustomThemeType,
|
|
};
|
|
|
|
export type MarkedDatesObjectType = {
|
|
[key: string]: {startingDay: boolean, endingDay: boolean, color: string},
|
|
};
|
|
|
|
type StateType = {
|
|
dialogVisible: boolean,
|
|
errorDialogVisible: boolean,
|
|
markedDates: MarkedDatesObjectType,
|
|
currentError: number,
|
|
};
|
|
|
|
class EquipmentRentScreen extends React.Component<PropsType, StateType> {
|
|
item: DeviceType | null;
|
|
|
|
bookedDates: Array<string>;
|
|
|
|
bookRef: {current: null | Animatable.View};
|
|
|
|
canBookEquipment: boolean;
|
|
|
|
lockedDates: {
|
|
[key: string]: {startingDay: boolean, endingDay: boolean, color: string},
|
|
};
|
|
|
|
constructor(props: PropsType) {
|
|
super(props);
|
|
this.state = {
|
|
dialogVisible: false,
|
|
errorDialogVisible: false,
|
|
markedDates: {},
|
|
currentError: 0,
|
|
};
|
|
this.resetSelection();
|
|
this.bookRef = React.createRef();
|
|
this.canBookEquipment = false;
|
|
this.bookedDates = [];
|
|
if (props.route.params != null) {
|
|
if (props.route.params.item != null) this.item = props.route.params.item;
|
|
else this.item = null;
|
|
}
|
|
const {item} = this;
|
|
if (item != null) {
|
|
this.lockedDates = {};
|
|
item.booked_at.forEach((date: {begin: string, end: string}) => {
|
|
const range = getValidRange(
|
|
new Date(date.begin),
|
|
new Date(date.end),
|
|
null,
|
|
);
|
|
this.lockedDates = {
|
|
...this.lockedDates,
|
|
...generateMarkedDates(false, props.theme, range),
|
|
};
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Captures focus and blur events to hook on android back button
|
|
*/
|
|
componentDidMount() {
|
|
const {navigation} = this.props;
|
|
navigation.addListener('focus', () => {
|
|
BackHandler.addEventListener(
|
|
'hardwareBackPress',
|
|
this.onBackButtonPressAndroid,
|
|
);
|
|
});
|
|
navigation.addListener('blur', () => {
|
|
BackHandler.removeEventListener(
|
|
'hardwareBackPress',
|
|
this.onBackButtonPressAndroid,
|
|
);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Overrides default android back button behaviour to deselect date if any is selected.
|
|
*
|
|
* @return {boolean}
|
|
*/
|
|
onBackButtonPressAndroid = (): boolean => {
|
|
if (this.bookedDates.length > 0) {
|
|
this.resetSelection();
|
|
this.updateMarkedSelection();
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
onDialogDismiss = () => {
|
|
this.setState({dialogVisible: false});
|
|
};
|
|
|
|
onErrorDialogDismiss = () => {
|
|
this.setState({errorDialogVisible: false});
|
|
};
|
|
|
|
/**
|
|
* Sends the selected data to the server and waits for a response.
|
|
* If the request is a success, navigate to the recap screen.
|
|
* If it is an error, display the error to the user.
|
|
*
|
|
* @returns {Promise<void>}
|
|
*/
|
|
onDialogAccept = (): Promise<void> => {
|
|
return new Promise((resolve: () => void) => {
|
|
const {item, props} = this;
|
|
const start = this.getBookStartDate();
|
|
const end = this.getBookEndDate();
|
|
if (item != null && start != null && end != null) {
|
|
ConnectionManager.getInstance()
|
|
.authenticatedRequest('location/booking', {
|
|
device: item.id,
|
|
begin: getISODate(start),
|
|
end: getISODate(end),
|
|
})
|
|
.then(() => {
|
|
this.onDialogDismiss();
|
|
props.navigation.replace('equipment-confirm', {
|
|
item: this.item,
|
|
dates: [getISODate(start), getISODate(end)],
|
|
});
|
|
resolve();
|
|
})
|
|
.catch((error: number) => {
|
|
this.onDialogDismiss();
|
|
this.showErrorDialog(error);
|
|
resolve();
|
|
});
|
|
} else {
|
|
this.onDialogDismiss();
|
|
resolve();
|
|
}
|
|
});
|
|
};
|
|
|
|
getBookStartDate(): Date | null {
|
|
return this.bookedDates.length > 0 ? new Date(this.bookedDates[0]) : null;
|
|
}
|
|
|
|
getBookEndDate(): Date | null {
|
|
const {length} = this.bookedDates;
|
|
return length > 0 ? new Date(this.bookedDates[length - 1]) : null;
|
|
}
|
|
|
|
/**
|
|
* Selects a new date on the calendar.
|
|
* If both start and end dates are already selected, unselect all.
|
|
*
|
|
* @param day The day selected
|
|
*/
|
|
selectNewDate = (day: {
|
|
dateString: string,
|
|
day: number,
|
|
month: number,
|
|
timestamp: number,
|
|
year: number,
|
|
}) => {
|
|
const selected = new Date(day.dateString);
|
|
const start = this.getBookStartDate();
|
|
|
|
if (!this.lockedDates[day.dateString] != null) {
|
|
if (start === null) {
|
|
this.updateSelectionRange(selected, selected);
|
|
this.enableBooking();
|
|
} else if (start.getTime() === selected.getTime()) {
|
|
this.resetSelection();
|
|
} else if (this.bookedDates.length === 1) {
|
|
this.updateSelectionRange(start, selected);
|
|
this.enableBooking();
|
|
} else this.resetSelection();
|
|
this.updateMarkedSelection();
|
|
}
|
|
};
|
|
|
|
showErrorDialog = (error: number) => {
|
|
this.setState({
|
|
errorDialogVisible: true,
|
|
currentError: error,
|
|
});
|
|
};
|
|
|
|
showDialog = () => {
|
|
this.setState({dialogVisible: true});
|
|
};
|
|
|
|
/**
|
|
* Shows the book button by plying a fade animation
|
|
*/
|
|
showBookButton() {
|
|
if (this.bookRef.current != null) {
|
|
this.bookRef.current.fadeInUp(500);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Hides the book button by plying a fade animation
|
|
*/
|
|
hideBookButton() {
|
|
if (this.bookRef.current != null) {
|
|
this.bookRef.current.fadeOutDown(500);
|
|
}
|
|
}
|
|
|
|
enableBooking() {
|
|
if (!this.canBookEquipment) {
|
|
this.showBookButton();
|
|
this.canBookEquipment = true;
|
|
}
|
|
}
|
|
|
|
resetSelection() {
|
|
if (this.canBookEquipment) this.hideBookButton();
|
|
this.canBookEquipment = false;
|
|
this.bookedDates = [];
|
|
}
|
|
|
|
updateSelectionRange(start: Date, end: Date) {
|
|
this.bookedDates = getValidRange(start, end, this.item);
|
|
}
|
|
|
|
updateMarkedSelection() {
|
|
const {theme} = this.props;
|
|
this.setState({
|
|
markedDates: generateMarkedDates(true, theme, this.bookedDates),
|
|
});
|
|
}
|
|
|
|
render(): React.Node {
|
|
const {item, props, state} = this;
|
|
const start = this.getBookStartDate();
|
|
const end = this.getBookEndDate();
|
|
let subHeadingText;
|
|
if (start == null) subHeadingText = i18n.t('screens.equipment.booking');
|
|
else if (end != null && start.getTime() !== end.getTime())
|
|
subHeadingText = i18n.t('screens.equipment.bookingPeriod', {
|
|
begin: getRelativeDateString(start),
|
|
end: getRelativeDateString(end),
|
|
});
|
|
else
|
|
i18n.t('screens.equipment.bookingDay', {
|
|
date: getRelativeDateString(start),
|
|
});
|
|
if (item != null) {
|
|
const isAvailable = isEquipmentAvailable(item);
|
|
const firstAvailability = getFirstEquipmentAvailability(item);
|
|
return (
|
|
<View style={{flex: 1}}>
|
|
<CollapsibleScrollView>
|
|
<Card style={{margin: 5}}>
|
|
<Card.Content>
|
|
<View style={{flex: 1}}>
|
|
<View
|
|
style={{
|
|
marginLeft: 'auto',
|
|
marginRight: 'auto',
|
|
flexDirection: 'row',
|
|
flexWrap: 'wrap',
|
|
}}>
|
|
<Headline style={{textAlign: 'center'}}>
|
|
{item.name}
|
|
</Headline>
|
|
<Caption
|
|
style={{
|
|
textAlign: 'center',
|
|
lineHeight: 35,
|
|
marginLeft: 10,
|
|
}}>
|
|
({i18n.t('screens.equipment.bail', {cost: item.caution})})
|
|
</Caption>
|
|
</View>
|
|
</View>
|
|
|
|
<Button
|
|
icon={isAvailable ? 'check-circle-outline' : 'update'}
|
|
color={
|
|
isAvailable
|
|
? props.theme.colors.success
|
|
: props.theme.colors.primary
|
|
}
|
|
mode="text">
|
|
{i18n.t('screens.equipment.available', {
|
|
date: getRelativeDateString(firstAvailability),
|
|
})}
|
|
</Button>
|
|
<Subheading
|
|
style={{
|
|
textAlign: 'center',
|
|
marginBottom: 10,
|
|
minHeight: 50,
|
|
}}>
|
|
{subHeadingText}
|
|
</Subheading>
|
|
</Card.Content>
|
|
</Card>
|
|
<CalendarList
|
|
// Minimum date that can be selected, dates before minDate will be grayed out. Default = undefined
|
|
minDate={new Date()}
|
|
// Max amount of months allowed to scroll to the past. Default = 50
|
|
pastScrollRange={0}
|
|
// Max amount of months allowed to scroll to the future. Default = 50
|
|
futureScrollRange={3}
|
|
// Enable horizontal scrolling, default = false
|
|
horizontal
|
|
// Enable paging on horizontal, default = false
|
|
pagingEnabled
|
|
// Handler which gets executed on day press. Default = undefined
|
|
onDayPress={this.selectNewDate}
|
|
// If firstDay=1 week starts from Monday. Note that dayNames and dayNamesShort should still start from Sunday.
|
|
firstDay={1}
|
|
// Disable all touch events for disabled days. can be override with disableTouchEvent in markedDates
|
|
disableAllTouchEventsForDisabledDays
|
|
// Hide month navigation arrows.
|
|
hideArrows={false}
|
|
// Date marking style [simple/period/multi-dot/custom]. Default = 'simple'
|
|
markingType="period"
|
|
markedDates={{...this.lockedDates, ...state.markedDates}}
|
|
theme={{
|
|
backgroundColor: props.theme.colors.agendaBackgroundColor,
|
|
calendarBackground: props.theme.colors.background,
|
|
textSectionTitleColor: props.theme.colors.agendaDayTextColor,
|
|
selectedDayBackgroundColor: props.theme.colors.primary,
|
|
selectedDayTextColor: '#ffffff',
|
|
todayTextColor: props.theme.colors.text,
|
|
dayTextColor: props.theme.colors.text,
|
|
textDisabledColor: props.theme.colors.agendaDayTextColor,
|
|
dotColor: props.theme.colors.primary,
|
|
selectedDotColor: '#ffffff',
|
|
arrowColor: props.theme.colors.primary,
|
|
monthTextColor: props.theme.colors.text,
|
|
indicatorColor: props.theme.colors.primary,
|
|
textDayFontFamily: 'monospace',
|
|
textMonthFontFamily: 'monospace',
|
|
textDayHeaderFontFamily: 'monospace',
|
|
textDayFontWeight: '300',
|
|
textMonthFontWeight: 'bold',
|
|
textDayHeaderFontWeight: '300',
|
|
textDayFontSize: 16,
|
|
textMonthFontSize: 16,
|
|
textDayHeaderFontSize: 16,
|
|
'stylesheet.day.period': {
|
|
base: {
|
|
overflow: 'hidden',
|
|
height: 34,
|
|
width: 34,
|
|
alignItems: 'center',
|
|
},
|
|
},
|
|
}}
|
|
style={{marginBottom: 50}}
|
|
/>
|
|
</CollapsibleScrollView>
|
|
<LoadingConfirmDialog
|
|
visible={state.dialogVisible}
|
|
onDismiss={this.onDialogDismiss}
|
|
onAccept={this.onDialogAccept}
|
|
title={i18n.t('screens.equipment.dialogTitle')}
|
|
titleLoading={i18n.t('screens.equipment.dialogTitleLoading')}
|
|
message={i18n.t('screens.equipment.dialogMessage')}
|
|
/>
|
|
|
|
<ErrorDialog
|
|
visible={state.errorDialogVisible}
|
|
onDismiss={this.onErrorDialogDismiss}
|
|
errorCode={state.currentError}
|
|
/>
|
|
<Animatable.View
|
|
ref={this.bookRef}
|
|
style={{
|
|
position: 'absolute',
|
|
bottom: 0,
|
|
left: 0,
|
|
width: '100%',
|
|
flex: 1,
|
|
transform: [{translateY: 100}],
|
|
}}>
|
|
<Button
|
|
icon="bookmark-check"
|
|
mode="contained"
|
|
onPress={this.showDialog}
|
|
style={{
|
|
width: '80%',
|
|
flex: 1,
|
|
marginLeft: 'auto',
|
|
marginRight: 'auto',
|
|
marginBottom: 20,
|
|
borderRadius: 10,
|
|
}}>
|
|
{i18n.t('screens.equipment.bookButton')}
|
|
</Button>
|
|
</Animatable.View>
|
|
</View>
|
|
);
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export default withTheme(EquipmentRentScreen);
|