forked from vergnet/application-amicale
569 lines
22 KiB
JavaScript
569 lines
22 KiB
JavaScript
// @flow
|
|
|
|
import * as React from 'react';
|
|
import {Button, Caption, Card, Headline, Subheading, Text, withTheme} from 'react-native-paper';
|
|
import {Collapsible} from "react-navigation-collapsible";
|
|
import {withCollapsible} from "../../../utils/withCollapsible";
|
|
import {StackNavigationProp} from "@react-navigation/stack";
|
|
import type {CustomTheme} from "../../../managers/ThemeManager";
|
|
import type {Device} from "./EquipmentListScreen";
|
|
import {Animated, BackHandler} from "react-native";
|
|
import * as Animatable from "react-native-animatable";
|
|
import {View} from "react-native-animatable";
|
|
import i18n from "i18n-js";
|
|
import {dateToString, getTimeOnlyString, stringToDate} from "../../../utils/Planning";
|
|
import {CalendarList} from "react-native-calendars";
|
|
import DateTimePicker from '@react-native-community/datetimepicker';
|
|
import LoadingConfirmDialog from "../../../components/Dialogs/LoadingConfirmDialog";
|
|
import ConnectionManager from "../../../managers/ConnectionManager";
|
|
import ErrorDialog from "../../../components/Dialogs/ErrorDialog";
|
|
|
|
type Props = {
|
|
navigation: StackNavigationProp,
|
|
route: {
|
|
params?: {
|
|
item?: Device,
|
|
},
|
|
},
|
|
theme: CustomTheme,
|
|
collapsibleStack: Collapsible,
|
|
}
|
|
|
|
type State = {
|
|
dialogVisible: boolean,
|
|
errorDialogVisible: boolean,
|
|
markedDates: { [key: string]: { startingDay: boolean, endingDay: boolean, color: string } },
|
|
timePickerVisible: boolean,
|
|
currentError: number,
|
|
}
|
|
|
|
class EquipmentRentScreen extends React.Component<Props, State> {
|
|
|
|
state = {
|
|
dialogVisible: false,
|
|
errorDialogVisible: false,
|
|
markedDates: {},
|
|
timePickerVisible: false,
|
|
currentError: 0,
|
|
}
|
|
|
|
item: Device | null;
|
|
selectedDates: {
|
|
start: Date | null,
|
|
end: Date | null,
|
|
};
|
|
|
|
currentlySelectedDate: Date | null;
|
|
|
|
bookRef: { current: null | Animatable.View }
|
|
canBookEquipment: boolean;
|
|
|
|
constructor(props: Props) {
|
|
super(props);
|
|
this.resetSelection();
|
|
this.bookRef = React.createRef();
|
|
this.canBookEquipment = false;
|
|
if (this.props.route.params != null) {
|
|
if (this.props.route.params.item != null)
|
|
this.item = this.props.route.params.item;
|
|
else
|
|
this.item = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Captures focus and blur events to hook on android back button
|
|
*/
|
|
componentDidMount() {
|
|
this.props.navigation.addListener(
|
|
'focus',
|
|
() =>
|
|
BackHandler.addEventListener(
|
|
'hardwareBackPress',
|
|
this.onBackButtonPressAndroid
|
|
)
|
|
);
|
|
this.props.navigation.addListener(
|
|
'blur',
|
|
() =>
|
|
BackHandler.removeEventListener(
|
|
'hardwareBackPress',
|
|
this.onBackButtonPressAndroid
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Overrides default android back button behaviour to deselect date if any is selected.
|
|
*
|
|
* @return {boolean}
|
|
*/
|
|
onBackButtonPressAndroid = () => {
|
|
if (this.currentlySelectedDate != null) {
|
|
this.resetSelection();
|
|
this.setState({
|
|
markedDates: this.generateMarkedDates(),
|
|
});
|
|
return true;
|
|
} else
|
|
return false;
|
|
};
|
|
|
|
isAvailable(item: Device) {
|
|
const availableDate = stringToDate(item.available_at);
|
|
return availableDate != null && availableDate < new Date();
|
|
}
|
|
|
|
/**
|
|
* Gets the string representation of the given date.
|
|
*
|
|
* If the given date is the same day as today, only return the tile.
|
|
* Otherwise, return the full date.
|
|
*
|
|
* @param dateString The string representation of the wanted date
|
|
* @returns {string}
|
|
*/
|
|
getDateString(dateString: string): string {
|
|
const today = new Date();
|
|
const date = stringToDate(dateString);
|
|
if (date != null && today.getDate() === date.getDate()) {
|
|
const str = getTimeOnlyString(dateString);
|
|
return str != null ? str : "";
|
|
} else
|
|
return dateString;
|
|
}
|
|
|
|
/**
|
|
* Gets the minimum date for renting equipment
|
|
*
|
|
* @param item The item to rent
|
|
* @param isAvailable True is it is available right now
|
|
* @returns {Date}
|
|
*/
|
|
getMinDate(item: Device, isAvailable: boolean) {
|
|
let date = new Date();
|
|
if (isAvailable)
|
|
return date;
|
|
else {
|
|
const limit = stringToDate(item.available_at)
|
|
return limit != null ? limit : date;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 }) => {
|
|
this.currentlySelectedDate = new Date(day.dateString);
|
|
|
|
if (!this.canBookEquipment) {
|
|
const start = this.selectedDates.start;
|
|
if (start == null)
|
|
this.selectedDates.start = this.currentlySelectedDate;
|
|
else if (this.currentlySelectedDate < start) {
|
|
this.selectedDates.end = start;
|
|
this.selectedDates.start = this.currentlySelectedDate;
|
|
} else
|
|
this.selectedDates.end = this.currentlySelectedDate;
|
|
} else
|
|
this.resetSelection();
|
|
|
|
if (this.selectedDates.start != null) {
|
|
this.setState({
|
|
markedDates: this.generateMarkedDates(),
|
|
timePickerVisible: true,
|
|
});
|
|
} else {
|
|
this.setState({
|
|
markedDates: this.generateMarkedDates(),
|
|
});
|
|
}
|
|
}
|
|
|
|
resetSelection() {
|
|
if (this.canBookEquipment)
|
|
this.hideBookButton();
|
|
this.canBookEquipment = false;
|
|
this.selectedDates = {start: null, end: null};
|
|
this.currentlySelectedDate = null;
|
|
}
|
|
|
|
/**
|
|
* Deselect the currently selected date
|
|
*/
|
|
deselectCurrentDate() {
|
|
let currentlySelectedDate = this.currentlySelectedDate;
|
|
const start = this.selectedDates.start;
|
|
const end = this.selectedDates.end;
|
|
if (currentlySelectedDate != null && start != null) {
|
|
if (currentlySelectedDate === start && end === null)
|
|
this.resetSelection();
|
|
else if (end != null && currentlySelectedDate === end) {
|
|
this.currentlySelectedDate = start;
|
|
this.selectedDates.end = null;
|
|
} else if (currentlySelectedDate === start) {
|
|
this.currentlySelectedDate = end;
|
|
this.selectedDates.start = this.selectedDates.end;
|
|
this.selectedDates.end = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Saves the selected time to the currently selected date.
|
|
* If no the time selection was canceled, cancels the current selecction
|
|
*
|
|
* @param event The click event
|
|
* @param date The date selected
|
|
*/
|
|
onTimeChange = (event: { nativeEvent: { timestamp: number }, type: string }, date: Date) => {
|
|
let currentDate = this.currentlySelectedDate;
|
|
const item = this.item;
|
|
if (item != null && event.type === "set" && currentDate != null) {
|
|
currentDate.setHours(date.getHours());
|
|
currentDate.setMinutes(date.getMinutes());
|
|
|
|
const isAvailable = this.isAvailable(item);
|
|
let limit = this.getMinDate(item, isAvailable);
|
|
// Prevent selecting a date before now
|
|
if (this.getISODate(currentDate) === this.getISODate(limit) && currentDate < limit) {
|
|
currentDate.setHours(limit.getHours());
|
|
currentDate.setMinutes(limit.getMinutes());
|
|
}
|
|
|
|
if (this.selectedDates.start != null && this.selectedDates.end != null) {
|
|
if (this.selectedDates.start > this.selectedDates.end) {
|
|
const temp = this.selectedDates.start;
|
|
this.selectedDates.start = this.selectedDates.end;
|
|
this.selectedDates.end = temp;
|
|
}
|
|
this.canBookEquipment = true;
|
|
this.showBookButton();
|
|
}
|
|
} else
|
|
this.deselectCurrentDate();
|
|
|
|
this.setState({
|
|
timePickerVisible: false,
|
|
markedDates: this.generateMarkedDates(),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Returns the ISO date format (without the time)
|
|
*
|
|
* @param date The date to recover the ISO format from
|
|
* @returns {*}
|
|
*/
|
|
getISODate(date: Date) {
|
|
return date.toISOString().split("T")[0];
|
|
}
|
|
|
|
/**
|
|
* Generates the object containing all marked dates between the start and end dates selected
|
|
*
|
|
* @returns {{}}
|
|
*/
|
|
generateMarkedDates() {
|
|
let markedDates = {}
|
|
const start = this.selectedDates.start;
|
|
const end = this.selectedDates.end;
|
|
if (start != null) {
|
|
const startISODate = this.getISODate(start);
|
|
if (end != null && this.getISODate(end) !== startISODate) {
|
|
markedDates[startISODate] = {
|
|
startingDay: true,
|
|
endingDay: false,
|
|
color: this.props.theme.colors.primary
|
|
};
|
|
markedDates[this.getISODate(end)] = {
|
|
startingDay: false,
|
|
endingDay: true,
|
|
color: this.props.theme.colors.primary
|
|
};
|
|
let date = new Date(start);
|
|
date.setDate(date.getDate() + 1);
|
|
while (date < end && this.getISODate(date) !== this.getISODate(end)) {
|
|
markedDates[this.getISODate(date)] =
|
|
{startingDay: false, endingDay: false, color: this.props.theme.colors.danger};
|
|
date.setDate(date.getDate() + 1);
|
|
}
|
|
} else {
|
|
markedDates[startISODate] = {
|
|
startingDay: true,
|
|
endingDay: true,
|
|
color: this.props.theme.colors.primary
|
|
};
|
|
}
|
|
}
|
|
return markedDates;
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
}
|
|
|
|
showDialog = () => {
|
|
this.setState({dialogVisible: true});
|
|
}
|
|
|
|
showErrorDialog = (error: number) => {
|
|
this.setState({
|
|
errorDialogVisible: true,
|
|
currentError: error,
|
|
});
|
|
}
|
|
|
|
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<R>}
|
|
*/
|
|
onDialogAccept = () => {
|
|
return new Promise((resolve) => {
|
|
const item = this.item;
|
|
const start = this.selectedDates.start;
|
|
const end = this.selectedDates.end;
|
|
if (item != null && start != null && end != null) {
|
|
ConnectionManager.getInstance().authenticatedRequest(
|
|
"", // TODO set path
|
|
{
|
|
"id": item.id,
|
|
"start": dateToString(start, false),
|
|
"end": dateToString(end, false),
|
|
})
|
|
.then(() => {
|
|
console.log("Success, replace screen");
|
|
resolve();
|
|
})
|
|
.catch((error: number) => {
|
|
this.onDialogDismiss();
|
|
this.showErrorDialog(error);
|
|
resolve();
|
|
});
|
|
} else
|
|
resolve();
|
|
});
|
|
}
|
|
|
|
render() {
|
|
const {containerPaddingTop, scrollIndicatorInsetTop, onScroll} = this.props.collapsibleStack;
|
|
let startString = <Caption>{i18n.t('equipmentScreen.notSet')}</Caption>;
|
|
let endString = <Caption>{i18n.t('equipmentScreen.notSet')}</Caption>;
|
|
const start = this.selectedDates.start;
|
|
const end = this.selectedDates.end;
|
|
if (start != null)
|
|
startString = dateToString(start, false);
|
|
if (end != null)
|
|
endString = dateToString(end, false);
|
|
|
|
const item = this.item;
|
|
if (item != null) {
|
|
const isAvailable = this.isAvailable(item);
|
|
return (
|
|
<View style={{flex: 1}}>
|
|
<Animated.ScrollView
|
|
// Animations
|
|
onScroll={onScroll}
|
|
contentContainerStyle={{
|
|
paddingTop: containerPaddingTop,
|
|
minHeight: '100%'
|
|
}}
|
|
scrollIndicatorInsets={{top: scrollIndicatorInsetTop}}>
|
|
<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('equipmentScreen.bail', {cost: item.caution})})
|
|
</Caption>
|
|
</View>
|
|
</View>
|
|
|
|
<Button
|
|
icon={isAvailable ? "check-circle-outline" : "update"}
|
|
color={isAvailable ? this.props.theme.colors.success : this.props.theme.colors.primary}
|
|
mode="text"
|
|
>
|
|
{
|
|
isAvailable
|
|
? i18n.t('equipmentScreen.available')
|
|
: i18n.t('equipmentScreen.availableAt', {date: this.getDateString(item.available_at)})
|
|
}
|
|
</Button>
|
|
<Text style={{
|
|
textAlign: "center",
|
|
marginBottom: 10
|
|
}}>
|
|
{i18n.t('equipmentScreen.booking')}
|
|
</Text>
|
|
<Subheading style={{textAlign: "center"}}>
|
|
{i18n.t('equipmentScreen.startDate')}
|
|
{startString}
|
|
</Subheading>
|
|
<Subheading style={{textAlign: "center"}}>
|
|
{i18n.t('equipmentScreen.endDate')}
|
|
{endString}
|
|
</Subheading>
|
|
</Card.Content>
|
|
</Card>
|
|
{this.state.timePickerVisible
|
|
? <DateTimePicker
|
|
value={new Date()}
|
|
mode={"time"}
|
|
display={"clock"}
|
|
is24Hour={true}
|
|
onChange={this.onTimeChange}
|
|
/>
|
|
: null}
|
|
<CalendarList
|
|
// Minimum date that can be selected, dates before minDate will be grayed out. Default = undefined
|
|
minDate={this.getMinDate(item, isAvailable)}
|
|
// 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={true}
|
|
// Enable paging on horizontal, default = false
|
|
pagingEnabled={true}
|
|
// 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={true}
|
|
// Hide month navigation arrows.
|
|
hideArrows={false}
|
|
// Date marking style [simple/period/multi-dot/custom]. Default = 'simple'
|
|
markingType={'period'}
|
|
markedDates={this.state.markedDates}
|
|
|
|
theme={{
|
|
backgroundColor: this.props.theme.colors.agendaBackgroundColor,
|
|
calendarBackground: this.props.theme.colors.background,
|
|
textSectionTitleColor: this.props.theme.colors.agendaDayTextColor,
|
|
selectedDayBackgroundColor: this.props.theme.colors.primary,
|
|
selectedDayTextColor: '#ffffff',
|
|
todayTextColor: this.props.theme.colors.text,
|
|
dayTextColor: this.props.theme.colors.text,
|
|
textDisabledColor: this.props.theme.colors.agendaDayTextColor,
|
|
dotColor: this.props.theme.colors.primary,
|
|
selectedDotColor: '#ffffff',
|
|
arrowColor: this.props.theme.colors.primary,
|
|
monthTextColor: this.props.theme.colors.text,
|
|
indicatorColor: this.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}}
|
|
/>
|
|
</Animated.ScrollView>
|
|
<LoadingConfirmDialog
|
|
visible={this.state.dialogVisible}
|
|
onDismiss={this.onDialogDismiss}
|
|
onAccept={this.onDialogAccept}
|
|
title={i18n.t('equipmentScreen.dialogTitle')}
|
|
titleLoading={i18n.t('equipmentScreen.dialogTitleLoading')}
|
|
message={i18n.t('equipmentScreen.dialogMessage')}
|
|
/>
|
|
|
|
<ErrorDialog
|
|
visible={this.state.errorDialogVisible}
|
|
onDismiss={this.onErrorDialogDismiss}
|
|
errorCode={this.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('equipmentScreen.bookButton')}
|
|
</Button>
|
|
</Animatable.View>
|
|
|
|
</View>
|
|
|
|
)
|
|
} else
|
|
return <View/>;
|
|
}
|
|
|
|
}
|
|
|
|
export default withCollapsible(withTheme(EquipmentRentScreen));
|