/* * Copyright (c) 2019 - 2020 Arnaud Vergnet. * * This file is part of Campus INSAT. * * Campus INSAT is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Campus INSAT is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Campus INSAT. If not, see . */ import * as React from 'react'; import { Button, Caption, Card, Headline, Subheading, withTheme, } from 'react-native-paper'; import {StackNavigationProp, StackScreenProps} from '@react-navigation/stack'; import {BackHandler, View} from 'react-native'; import * as Animatable from 'react-native-animatable'; import i18n from 'i18n-js'; import {CalendarList, PeriodMarking} from 'react-native-calendars'; import type {DeviceType} from './EquipmentListScreen'; 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'; import {MainStackParamsList} from '../../../navigation/MainNavigator'; type EquipmentRentScreenNavigationProp = StackScreenProps< MainStackParamsList, 'equipment-rent' >; type Props = EquipmentRentScreenNavigationProp & { navigation: StackNavigationProp; theme: ReactNativePaper.Theme; }; export type MarkedDatesObjectType = { [key: string]: PeriodMarking; }; 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 & View)}; canBookEquipment: boolean; lockedDates: { [key: string]: PeriodMarking; }; constructor(props: Props) { super(props); this.item = null; this.lockedDates = {}; 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 && this.bookRef.current.fadeInUp) { this.bookRef.current.fadeInUp(500); } } /** * Hides the book button by plying a fade animation */ hideBookButton() { if (this.bookRef.current && this.bookRef.current.fadeOutDown) { 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() { 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 { subHeadingText = 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);