// @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 { item: DeviceType | null; bookedDates: Array; 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} */ onDialogAccept = (): Promise => { 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 ( {item.name} ({i18n.t('screens.equipment.bail', {cost: item.caution})}) {subHeadingText} ); } return null; } } export default withTheme(EquipmentRentScreen);