// @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 { 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} */ 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 = {i18n.t('equipmentScreen.notSet')}; let endString = {i18n.t('equipmentScreen.notSet')}; 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 ( {item.name} ({i18n.t('equipmentScreen.bail', {cost: item.caution})}) {i18n.t('equipmentScreen.booking')} {i18n.t('equipmentScreen.startDate')} {startString} {i18n.t('equipmentScreen.endDate')} {endString} {this.state.timePickerVisible ? : null} ) } else return ; } } export default withCollapsible(withTheme(EquipmentRentScreen));