/* * 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, StyleSheet, 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'; import GENERAL_STYLES from '../../../constants/Styles'; 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; }; const styles = StyleSheet.create({ titleContainer: { marginLeft: 'auto', marginRight: 'auto', flexDirection: 'row', flexWrap: 'wrap', }, title: { textAlign: 'center', }, caption: { textAlign: 'center', lineHeight: 35, marginLeft: 10, }, card: { margin: 5, }, subtitle: { textAlign: 'center', marginBottom: 10, minHeight: 50, }, calendar: { marginBottom: 50, }, buttonContainer: { position: 'absolute', bottom: 0, left: 0, width: '100%', flex: 1, transform: [{ translateY: 100 }], }, button: { width: '80%', flex: 1, marginLeft: 'auto', marginRight: 'auto', marginBottom: 20, borderRadius: 10, }, }); 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);