diff --git a/src/components/Lists/Equipment/EquipmentListItem.js b/src/components/Lists/Equipment/EquipmentListItem.js index 988f958..e1c32fa 100644 --- a/src/components/Lists/Equipment/EquipmentListItem.js +++ b/src/components/Lists/Equipment/EquipmentListItem.js @@ -2,111 +2,112 @@ import * as React from 'react'; import {Avatar, List, withTheme} from 'react-native-paper'; -import type {CustomTheme} from "../../../managers/ThemeManager"; -import type {Device} from "../../../screens/Amicale/Equipment/EquipmentListScreen"; -import i18n from "i18n-js"; +import i18n from 'i18n-js'; +import {StackNavigationProp} from '@react-navigation/stack'; +import type {CustomTheme} from '../../../managers/ThemeManager'; +import type {DeviceType} from '../../../screens/Amicale/Equipment/EquipmentListScreen'; import { - getFirstEquipmentAvailability, - getRelativeDateString, - isEquipmentAvailable -} from "../../../utils/EquipmentBooking"; -import {StackNavigationProp} from "@react-navigation/stack"; + getFirstEquipmentAvailability, + getRelativeDateString, + isEquipmentAvailable, +} from '../../../utils/EquipmentBooking'; -type Props = { - navigation: StackNavigationProp, - userDeviceRentDates: [string, string], - item: Device, - height: number, - theme: CustomTheme, -} +type PropsType = { + navigation: StackNavigationProp, + userDeviceRentDates: [string, string], + item: DeviceType, + height: number, + theme: CustomTheme, +}; -class EquipmentListItem extends React.Component { +class EquipmentListItem extends React.Component { + shouldComponentUpdate(nextProps: PropsType): boolean { + const {userDeviceRentDates} = this.props; + return nextProps.userDeviceRentDates !== userDeviceRentDates; + } - shouldComponentUpdate(nextProps: Props): boolean { - return nextProps.userDeviceRentDates !== this.props.userDeviceRentDates; - } + render(): React.Node { + const {item, userDeviceRentDates, navigation, height, theme} = this.props; + const isRented = userDeviceRentDates != null; + const isAvailable = isEquipmentAvailable(item); + const firstAvailability = getFirstEquipmentAvailability(item); - render() { - const colors = this.props.theme.colors; - const item = this.props.item; - const userDeviceRentDates = this.props.userDeviceRentDates; - const isRented = userDeviceRentDates != null; - const isAvailable = isEquipmentAvailable(item); - const firstAvailability = getFirstEquipmentAvailability(item); + let onPress; + if (isRented) + onPress = () => { + navigation.navigate('equipment-confirm', { + item, + dates: userDeviceRentDates, + }); + }; + else + onPress = () => { + navigation.navigate('equipment-rent', {item}); + }; - let onPress; - if (isRented) - onPress = () => this.props.navigation.navigate("equipment-confirm", { - item: item, - dates: userDeviceRentDates - }); - else - onPress = () => this.props.navigation.navigate("equipment-rent", {item: item}); + let description; + if (isRented) { + const start = new Date(userDeviceRentDates[0]); + const end = new Date(userDeviceRentDates[1]); + if (start.getTime() !== end.getTime()) + description = i18n.t('screens.equipment.bookingPeriod', { + begin: getRelativeDateString(start), + end: getRelativeDateString(end), + }); + else + description = i18n.t('screens.equipment.bookingDay', { + date: getRelativeDateString(start), + }); + } else if (isAvailable) + description = i18n.t('screens.equipment.bail', {cost: item.caution}); + else + description = i18n.t('screens.equipment.available', { + date: getRelativeDateString(firstAvailability), + }); - let description; - if (isRented) { - const start = new Date(userDeviceRentDates[0]); - const end = new Date(userDeviceRentDates[1]); - if (start.getTime() !== end.getTime()) - description = i18n.t('screens.equipment.bookingPeriod', { - begin: getRelativeDateString(start), - end: getRelativeDateString(end) - }); - else - description = i18n.t('screens.equipment.bookingDay', { - date: getRelativeDateString(start) - }); - } else if (isAvailable) - description = i18n.t('screens.equipment.bail', {cost: item.caution}); - else - description = i18n.t('screens.equipment.available', {date: getRelativeDateString(firstAvailability)}); + let icon; + if (isRented) icon = 'bookmark-check'; + else if (isAvailable) icon = 'check-circle-outline'; + else icon = 'update'; - let icon; - if (isRented) - icon = "bookmark-check"; - else if (isAvailable) - icon = "check-circle-outline"; - else - icon = "update"; + let color; + if (isRented) color = theme.colors.warning; + else if (isAvailable) color = theme.colors.success; + else color = theme.colors.primary; - let color; - if (isRented) - color = colors.warning; - else if (isAvailable) - color = colors.success; - else - color = colors.primary; - - return ( - } - right={(props) => } - style={{ - height: this.props.height, - justifyContent: 'center', - }} - /> - ); - } + return ( + ( + + )} + right={(): React.Node => ( + + )} + style={{ + height, + justifyContent: 'center', + }} + /> + ); + } } export default withTheme(EquipmentListItem); diff --git a/src/screens/Amicale/Equipment/EquipmentConfirmScreen.js b/src/screens/Amicale/Equipment/EquipmentConfirmScreen.js index ecccf39..506391f 100644 --- a/src/screens/Amicale/Equipment/EquipmentConfirmScreen.js +++ b/src/screens/Amicale/Equipment/EquipmentConfirmScreen.js @@ -1,105 +1,102 @@ // @flow import * as React from 'react'; -import {Button, Caption, Card, Headline, Paragraph, withTheme} from 'react-native-paper'; -import {StackNavigationProp} from "@react-navigation/stack"; -import type {CustomTheme} from "../../../managers/ThemeManager"; -import type {Device} from "./EquipmentListScreen"; -import {View} from "react-native"; -import i18n from "i18n-js"; -import {getRelativeDateString} from "../../../utils/EquipmentBooking"; -import CollapsibleScrollView from "../../../components/Collapsible/CollapsibleScrollView"; +import { + Button, + Caption, + Card, + Headline, + Paragraph, + withTheme, +} from 'react-native-paper'; +import {View} from 'react-native'; +import i18n from 'i18n-js'; +import type {CustomTheme} from '../../../managers/ThemeManager'; +import type {DeviceType} from './EquipmentListScreen'; +import {getRelativeDateString} from '../../../utils/EquipmentBooking'; +import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView'; -type Props = { - navigation: StackNavigationProp, - route: { - params?: { - item?: Device, - dates: [string, string] - }, +type PropsType = { + route: { + params?: { + item?: DeviceType, + dates: [string, string], }, - theme: CustomTheme, -} + }, + theme: CustomTheme, +}; +class EquipmentConfirmScreen extends React.Component { + item: DeviceType | null; -class EquipmentConfirmScreen extends React.Component { + dates: [string, string] | null; - item: Device | null; - dates: [string, string] | null; - - constructor(props: Props) { - super(props); - if (this.props.route.params != null) { - if (this.props.route.params.item != null) - this.item = this.props.route.params.item; - else - this.item = null; - if (this.props.route.params.dates != null) - this.dates = this.props.route.params.dates; - else - this.dates = null; - } + constructor(props: PropsType) { + super(props); + if (props.route.params != null) { + if (props.route.params.item != null) this.item = props.route.params.item; + else this.item = null; + if (props.route.params.dates != null) + this.dates = props.route.params.dates; + else this.dates = null; } + } - render() { - const item = this.item; - const dates = this.dates; - if (item != null && dates != null) { - const start = new Date(dates[0]); - const end = new Date(dates[1]); - return ( - - - - - - - {item.name} - - - ({i18n.t('screens.equipment.bail', {cost: item.caution})}) - - - - - - {i18n.t("screens.equipment.bookingConfirmedMessage")} - - - - - ); - } else - return null; - + render(): React.Node { + const {item, dates, props} = this; + if (item != null && dates != null) { + const start = new Date(dates[0]); + const end = new Date(dates[1]); + let buttonText; + if (start == null) buttonText = i18n.t('screens.equipment.booking'); + else if (end != null && start.getTime() !== end.getTime()) + buttonText = i18n.t('screens.equipment.bookingPeriod', { + begin: getRelativeDateString(start), + end: getRelativeDateString(end), + }); + else + buttonText = i18n.t('screens.equipment.bookingDay', { + date: getRelativeDateString(start), + }); + return ( + + + + + + {item.name} + + ({i18n.t('screens.equipment.bail', {cost: item.caution})}) + + + + + + {i18n.t('screens.equipment.bookingConfirmedMessage')} + + + + + ); } - + return null; + } } export default withTheme(EquipmentConfirmScreen); diff --git a/src/screens/Amicale/Equipment/EquipmentListScreen.js b/src/screens/Amicale/Equipment/EquipmentListScreen.js index 3643879..b773faf 100644 --- a/src/screens/Amicale/Equipment/EquipmentListScreen.js +++ b/src/screens/Amicale/Equipment/EquipmentListScreen.js @@ -1,193 +1,197 @@ // @flow import * as React from 'react'; -import {View} from "react-native"; +import {View} from 'react-native'; import {Button, withTheme} from 'react-native-paper'; -import AuthenticatedScreen from "../../../components/Amicale/AuthenticatedScreen"; -import {StackNavigationProp} from "@react-navigation/stack"; -import type {CustomTheme} from "../../../managers/ThemeManager"; -import i18n from "i18n-js"; -import type {club} from "../Clubs/ClubListScreen"; -import EquipmentListItem from "../../../components/Lists/Equipment/EquipmentListItem"; -import MascotPopup from "../../../components/Mascot/MascotPopup"; -import {MASCOT_STYLE} from "../../../components/Mascot/Mascot"; -import AsyncStorageManager from "../../../managers/AsyncStorageManager"; -import CollapsibleFlatList from "../../../components/Collapsible/CollapsibleFlatList"; +import {StackNavigationProp} from '@react-navigation/stack'; +import i18n from 'i18n-js'; +import AuthenticatedScreen from '../../../components/Amicale/AuthenticatedScreen'; +import type {ClubType} from '../Clubs/ClubListScreen'; +import EquipmentListItem from '../../../components/Lists/Equipment/EquipmentListItem'; +import MascotPopup from '../../../components/Mascot/MascotPopup'; +import {MASCOT_STYLE} from '../../../components/Mascot/Mascot'; +import AsyncStorageManager from '../../../managers/AsyncStorageManager'; +import CollapsibleFlatList from '../../../components/Collapsible/CollapsibleFlatList'; +import type {ApiGenericDataType} from '../../../utils/WebData'; -type Props = { - navigation: StackNavigationProp, - theme: CustomTheme, -} - -type State = { - mascotDialogVisible: boolean, -} - -export type Device = { - id: number, - name: string, - caution: number, - booked_at: Array<{ begin: string, end: string }>, +type PropsType = { + navigation: StackNavigationProp, }; -export type RentedDevice = { - device_id: number, - device_name: string, - begin: string, - end: string, -} +type StateType = { + mascotDialogVisible: boolean, +}; + +export type DeviceType = { + id: number, + name: string, + caution: number, + booked_at: Array<{begin: string, end: string}>, +}; + +export type RentedDeviceType = { + device_id: number, + device_name: string, + begin: string, + end: string, +}; const LIST_ITEM_HEIGHT = 64; -class EquipmentListScreen extends React.Component { +class EquipmentListScreen extends React.Component { + data: Array; - state = { - mascotDialogVisible: AsyncStorageManager.getBool(AsyncStorageManager.PREFERENCES.equipmentShowBanner.key), - } + userRents: Array; - data: Array; - userRents: Array; + authRef: {current: null | AuthenticatedScreen}; - authRef: { current: null | AuthenticatedScreen }; - canRefresh: boolean; + canRefresh: boolean; - constructor(props: Props) { - super(props); - this.canRefresh = false; - this.authRef = React.createRef(); - this.props.navigation.addListener('focus', this.onScreenFocus); - } - - onScreenFocus = () => { - if (this.canRefresh && this.authRef.current != null) - this.authRef.current.reload(); - this.canRefresh = true; + constructor(props: PropsType) { + super(props); + this.state = { + mascotDialogVisible: AsyncStorageManager.getBool( + AsyncStorageManager.PREFERENCES.equipmentShowBanner.key, + ), }; + this.canRefresh = false; + this.authRef = React.createRef(); + props.navigation.addListener('focus', this.onScreenFocus); + } - getRenderItem = ({item}: { item: Device }) => { - return ( - - ); - }; + onScreenFocus = () => { + if (this.canRefresh && this.authRef.current != null) + this.authRef.current.reload(); + this.canRefresh = true; + }; - getUserDeviceRentDates(item: Device) { - let dates = null; - for (let i = 0; i < this.userRents.length; i++) { - let device = this.userRents[i]; - if (item.id === device.device_id) { - dates = [device.begin, device.end]; - break; - } - } - return dates; + getRenderItem = ({item}: {item: DeviceType}): React.Node => { + const {navigation} = this.props; + return ( + + ); + }; + + getUserDeviceRentDates(item: DeviceType): [number, number] | null { + let dates = null; + this.userRents.forEach((device: RentedDeviceType) => { + if (item.id === device.device_id) { + dates = [device.begin, device.end]; + } + }); + return dates; + } + + /** + * Gets the list header, with explains this screen's purpose + * + * @returns {*} + */ + getListHeader(): React.Node { + return ( + + + + ); + } + + keyExtractor = (item: ClubType): string => item.id.toString(); + + /** + * Gets the main screen component with the fetched data + * + * @param data The data fetched from the server + * @returns {*} + */ + getScreen = (data: Array): React.Node => { + if (data[0] != null) { + const fetchedData = data[0]; + if (fetchedData != null) this.data = fetchedData.devices; } - - /** - * Gets the list header, with explains this screen's purpose - * - * @returns {*} - */ - getListHeader() { - return ( - - - - ); + if (data[1] != null) { + const fetchedData = data[1]; + if (fetchedData != null) this.userRents = fetchedData.locations; } + return ( + + ); + }; - keyExtractor = (item: club) => item.id.toString(); + showMascotDialog = () => { + this.setState({mascotDialogVisible: true}); + }; - /** - * Gets the main screen component with the fetched data - * - * @param data The data fetched from the server - * @returns {*} - */ - getScreen = (data: Array<{ [key: string]: any } | null>) => { - if (data[0] != null) { - const fetchedData = data[0]; - if (fetchedData != null) - this.data = fetchedData["devices"]; - } - if (data[1] != null) { - const fetchedData = data[1]; - if (fetchedData != null) - this.userRents = fetchedData["locations"]; - } - return ( - - ) - }; + hideMascotDialog = () => { + AsyncStorageManager.set( + AsyncStorageManager.PREFERENCES.equipmentShowBanner.key, + false, + ); + this.setState({mascotDialogVisible: false}); + }; - showMascotDialog = () => { - this.setState({mascotDialogVisible: true}) - }; - - hideMascotDialog = () => { - AsyncStorageManager.set(AsyncStorageManager.PREFERENCES.equipmentShowBanner.key, false); - this.setState({mascotDialogVisible: false}) - }; - - render() { - return ( - - - - - ); - } + render(): React.Node { + const {props, state} = this; + return ( + + + + + ); + } } export default withTheme(EquipmentListScreen); diff --git a/src/screens/Amicale/Equipment/EquipmentRentScreen.js b/src/screens/Amicale/Equipment/EquipmentRentScreen.js index b94b030..5c102b4 100644 --- a/src/screens/Amicale/Equipment/EquipmentRentScreen.js +++ b/src/screens/Amicale/Equipment/EquipmentRentScreen.js @@ -1,441 +1,441 @@ // @flow import * as React from 'react'; -import {Button, Caption, Card, Headline, Subheading, withTheme} from 'react-native-paper'; -import {StackNavigationProp} from "@react-navigation/stack"; -import type {CustomTheme} from "../../../managers/ThemeManager"; -import type {Device} from "./EquipmentListScreen"; -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 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"; + 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 {CustomTheme} 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 Props = { - navigation: StackNavigationProp, - route: { - params?: { - item?: Device, - }, +type PropsType = { + navigation: StackNavigationProp, + route: { + params?: { + item?: DeviceType, }, - theme: CustomTheme, -} + }, + theme: CustomTheme, +}; -type State = { - dialogVisible: boolean, - errorDialogVisible: boolean, - markedDates: { [key: string]: { startingDay: boolean, endingDay: boolean, color: string } }, - currentError: number, -} +export type MarkedDatesObjectType = { + [key: string]: {startingDay: boolean, endingDay: boolean, color: string}, +}; -class EquipmentRentScreen extends React.Component { +type StateType = { + dialogVisible: boolean, + errorDialogVisible: boolean, + markedDates: MarkedDatesObjectType, + currentError: number, +}; - state = { - dialogVisible: false, - errorDialogVisible: false, - markedDates: {}, - currentError: 0, - } +class EquipmentRentScreen extends React.Component { + item: DeviceType | null; - item: Device | null; - bookedDates: Array; + bookedDates: Array; - bookRef: { current: null | Animatable.View } - canBookEquipment: boolean; + bookRef: {current: null | Animatable.View}; - lockedDates: { [key: string]: { startingDay: boolean, endingDay: boolean, color: string } } + canBookEquipment: boolean; - constructor(props: Props) { - super(props); - this.resetSelection(); - this.bookRef = React.createRef(); - this.canBookEquipment = false; - this.bookedDates = []; - if (this.props.route.params != null) { - if (this.props.route.params.item != null) - this.item = this.props.route.params.item; - else - this.item = null; - } - const item = this.item; - if (item != null) { - this.lockedDates = {}; - for (let i = 0; i < item.booked_at.length; i++) { - const range = getValidRange(new Date(item.booked_at[i].begin), new Date(item.booked_at[i].end), null); - this.lockedDates = { - ...this.lockedDates, - ...generateMarkedDates( - false, - this.props.theme, - range - ) - }; - } - } + lockedDates: { + [key: string]: {startingDay: boolean, endingDay: boolean, color: string}, + }; - } - - /** - * 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.bookedDates.length > 0) { - this.resetSelection(); - this.updateMarkedSelection(); - return true; - } else - return false; + constructor(props: PropsType) { + super(props); + this.state = { + dialogVisible: false, + errorDialogVisible: false, + markedDates: {}, + currentError: 0, }; - - /** - * 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.hasOwnProperty(day.dateString))) { - 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(); - } + 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; } - - updateSelectionRange(start: Date, end: Date) { - this.bookedDates = getValidRange(start, end, this.item); + 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), + }; + }); } + } - updateMarkedSelection() { - this.setState({ - markedDates: generateMarkedDates( - true, - this.props.theme, - this.bookedDates - ), - }); + /** + * 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; + }; - enableBooking() { - if (!this.canBookEquipment) { - this.showBookButton(); - this.canBookEquipment = true; - } + 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(); } + }; - resetSelection() { - if (this.canBookEquipment) - this.hideBookButton(); - this.canBookEquipment = false; - this.bookedDates = []; + 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); } + } - /** - * 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); } + } - /** - * 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; } + } - showDialog = () => { - this.setState({dialogVisible: true}); - } + resetSelection() { + if (this.canBookEquipment) this.hideBookButton(); + this.canBookEquipment = false; + this.bookedDates = []; + } - showErrorDialog = (error: number) => { - this.setState({ - errorDialogVisible: true, - currentError: error, - }); - } + updateSelectionRange(start: Date, end: Date) { + this.bookedDates = getValidRange(start, end, this.item); + } - onDialogDismiss = () => { - this.setState({dialogVisible: false}); - } + updateMarkedSelection() { + const {theme} = this.props; + this.setState({ + markedDates: generateMarkedDates(true, theme, this.bookedDates), + }); + } - 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.getBookStartDate(); - const end = this.getBookEndDate(); - if (item != null && start != null && end != null) { - console.log({ - "device": item.id, - "begin": getISODate(start), - "end": getISODate(end), - }) - ConnectionManager.getInstance().authenticatedRequest( - "location/booking", - { - "device": item.id, - "begin": getISODate(start), - "end": getISODate(end), - }) - .then(() => { - this.onDialogDismiss(); - this.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() { - return this.bookedDates.length > 0 ? new Date(this.bookedDates[0]) : null; - } - - getBookEndDate() { - const length = this.bookedDates.length; - return length > 0 ? new Date(this.bookedDates[length - 1]) : null; - } - - render() { - const item = this.item; - const start = this.getBookStartDate(); - const end = this.getBookEndDate(); - - if (item != null) { - const isAvailable = isEquipmentAvailable(item); - const firstAvailability = getFirstEquipmentAvailability(item); - return ( + 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})}) - - - - - - - { - start == null - ? i18n.t('screens.equipment.booking') - : end != null && start.getTime() !== end.getTime() - ? i18n.t('screens.equipment.bookingPeriod', { - begin: getRelativeDateString(start), - end: getRelativeDateString(end) - }) - : i18n.t('screens.equipment.bookingDay', { - date: getRelativeDateString(start) - }) - } - - - - - - - - - - - - - + + + {item.name} + + + ({i18n.t('screens.equipment.bail', {cost: item.caution})}) + + - ) - } else - return ; - } + + + {subHeadingText} + + + + + + + + + + + + ); + } + return null; + } } export default withTheme(EquipmentRentScreen); diff --git a/src/utils/EquipmentBooking.js b/src/utils/EquipmentBooking.js index 28e2532..fd31610 100644 --- a/src/utils/EquipmentBooking.js +++ b/src/utils/EquipmentBooking.js @@ -1,19 +1,20 @@ // @flow -import type {Device} from "../screens/Amicale/Equipment/EquipmentListScreen"; -import i18n from "i18n-js"; -import DateManager from "../managers/DateManager"; -import type {CustomTheme} from "../managers/ThemeManager"; +import i18n from 'i18n-js'; +import type {DeviceType} from '../screens/Amicale/Equipment/EquipmentListScreen'; +import DateManager from '../managers/DateManager'; +import type {CustomTheme} from '../managers/ThemeManager'; +import type {MarkedDatesObjectType} from '../screens/Amicale/Equipment/EquipmentRentScreen'; /** * Gets the current day at midnight * * @returns {Date} */ -export function getCurrentDay() { - let today = new Date(Date.now()); - today.setUTCHours(0, 0, 0, 0); - return today; +export function getCurrentDay(): Date { + const today = new Date(Date.now()); + today.setUTCHours(0, 0, 0, 0); + return today; } /** @@ -22,8 +23,8 @@ export function getCurrentDay() { * @param date The date to recover the ISO format from * @returns {*} */ -export function getISODate(date: Date) { - return date.toISOString().split("T")[0]; +export function getISODate(date: Date): string { + return date.toISOString().split('T')[0]; } /** @@ -32,18 +33,16 @@ export function getISODate(date: Date) { * @param item * @returns {boolean} */ -export function isEquipmentAvailable(item: Device) { - let isAvailable = true; - const today = getCurrentDay(); - const dates = item.booked_at; - for (let i = 0; i < dates.length; i++) { - const start = new Date(dates[i].begin); - const end = new Date(dates[i].end); - isAvailable = today < start || today > end; - if (!isAvailable) - break; - } - return isAvailable; +export function isEquipmentAvailable(item: DeviceType): boolean { + let isAvailable = true; + const today = getCurrentDay(); + const dates = item.booked_at; + dates.forEach((date: {begin: string, end: string}) => { + const start = new Date(date.begin); + const end = new Date(date.end); + if (!(today < start || today > end)) isAvailable = false; + }); + return isAvailable; } /** @@ -52,17 +51,16 @@ export function isEquipmentAvailable(item: Device) { * @param item * @returns {Date} */ -export function getFirstEquipmentAvailability(item: Device) { - let firstAvailability = getCurrentDay(); - const dates = item.booked_at; - for (let i = 0; i < dates.length; i++) { - const start = new Date(dates[i].begin); - let end = new Date(dates[i].end); - end.setDate(end.getDate() + 1); - if (firstAvailability >= start) - firstAvailability = end; - } - return firstAvailability; +export function getFirstEquipmentAvailability(item: DeviceType): Date { + let firstAvailability = getCurrentDay(); + const dates = item.booked_at; + dates.forEach((date: {begin: string, end: string}) => { + const start = new Date(date.begin); + const end = new Date(date.end); + end.setDate(end.getDate() + 1); + if (firstAvailability >= start) firstAvailability = end; + }); + return firstAvailability; } /** @@ -70,31 +68,31 @@ export function getFirstEquipmentAvailability(item: Device) { * * @param date The date to translate */ -export function getRelativeDateString(date: Date) { - const today = getCurrentDay(); - const yearDelta = date.getUTCFullYear() - today.getUTCFullYear(); - const monthDelta = date.getUTCMonth() - today.getUTCMonth(); - const dayDelta = date.getUTCDate() - today.getUTCDate(); - let translatedString = i18n.t('screens.equipment.today'); - if (yearDelta > 0) - translatedString = i18n.t('screens.equipment.otherYear', { - date: date.getDate(), - month: DateManager.getInstance().getMonthsOfYear()[date.getMonth()], - year: date.getFullYear() - }); - else if (monthDelta > 0) - translatedString = i18n.t('screens.equipment.otherMonth', { - date: date.getDate(), - month: DateManager.getInstance().getMonthsOfYear()[date.getMonth()], - }); - else if (dayDelta > 1) - translatedString = i18n.t('screens.equipment.thisMonth', { - date: date.getDate(), - }); - else if (dayDelta === 1) - translatedString = i18n.t('screens.equipment.tomorrow'); +export function getRelativeDateString(date: Date): string { + const today = getCurrentDay(); + const yearDelta = date.getUTCFullYear() - today.getUTCFullYear(); + const monthDelta = date.getUTCMonth() - today.getUTCMonth(); + const dayDelta = date.getUTCDate() - today.getUTCDate(); + let translatedString = i18n.t('screens.equipment.today'); + if (yearDelta > 0) + translatedString = i18n.t('screens.equipment.otherYear', { + date: date.getDate(), + month: DateManager.getInstance().getMonthsOfYear()[date.getMonth()], + year: date.getFullYear(), + }); + else if (monthDelta > 0) + translatedString = i18n.t('screens.equipment.otherMonth', { + date: date.getDate(), + month: DateManager.getInstance().getMonthsOfYear()[date.getMonth()], + }); + else if (dayDelta > 1) + translatedString = i18n.t('screens.equipment.thisMonth', { + date: date.getDate(), + }); + else if (dayDelta === 1) + translatedString = i18n.t('screens.equipment.tomorrow'); - return translatedString; + return translatedString; } /** @@ -111,41 +109,45 @@ export function getRelativeDateString(date: Date) { * @param item Item containing booked dates to look for * @returns {[string]} */ -export function getValidRange(start: Date, end: Date, item: Device | null) { - let direction = start <= end ? 1 : -1; - let limit = new Date(end); - limit.setDate(limit.getDate() + direction); // Limit is excluded, but we want to include range end - if (item != null) { - if (direction === 1) { - for (let i = 0; i < item.booked_at.length; i++) { - const bookLimit = new Date(item.booked_at[i].begin); - if (start < bookLimit && limit > bookLimit) { - limit = bookLimit; - break; - } - } - } else { - for (let i = item.booked_at.length - 1; i >= 0; i--) { - const bookLimit = new Date(item.booked_at[i].end); - if (start > bookLimit && limit < bookLimit) { - limit = bookLimit; - break; - } - } +export function getValidRange( + start: Date, + end: Date, + item: DeviceType | null, +): Array { + const direction = start <= end ? 1 : -1; + let limit = new Date(end); + limit.setDate(limit.getDate() + direction); // Limit is excluded, but we want to include range end + if (item != null) { + if (direction === 1) { + for (let i = 0; i < item.booked_at.length; i += 1) { + const bookLimit = new Date(item.booked_at[i].begin); + if (start < bookLimit && limit > bookLimit) { + limit = bookLimit; + break; } + } + } else { + for (let i = item.booked_at.length - 1; i >= 0; i -= 1) { + const bookLimit = new Date(item.booked_at[i].end); + if (start > bookLimit && limit < bookLimit) { + limit = bookLimit; + break; + } + } } + } - - let validRange = []; - let date = new Date(start); - while ((direction === 1 && date < limit) || (direction === -1 && date > limit)) { - if (direction === 1) - validRange.push(getISODate(date)); - else - validRange.unshift(getISODate(date)); - date.setDate(date.getDate() + direction); - } - return validRange; + const validRange = []; + const date = new Date(start); + while ( + (direction === 1 && date < limit) || + (direction === -1 && date > limit) + ) { + if (direction === 1) validRange.push(getISODate(date)); + else validRange.unshift(getISODate(date)); + date.setDate(date.getDate() + direction); + } + return validRange; } /** @@ -157,20 +159,24 @@ export function getValidRange(start: Date, end: Date, item: Device | null) { * @param range The range to mark dates for * @returns {{}} */ -export function generateMarkedDates(isSelection: boolean, theme: CustomTheme, range: Array) { - let markedDates = {} - for (let i = 0; i < range.length; i++) { - const isStart = i === 0; - const isEnd = i === range.length - 1; - markedDates[range[i]] = { - startingDay: isStart, - endingDay: isEnd, - color: isSelection - ? isStart || isEnd - ? theme.colors.primary - : theme.colors.danger - : theme.colors.textDisabled - }; - } - return markedDates; +export function generateMarkedDates( + isSelection: boolean, + theme: CustomTheme, + range: Array, +): MarkedDatesObjectType { + const markedDates = {}; + for (let i = 0; i < range.length; i += 1) { + const isStart = i === 0; + const isEnd = i === range.length - 1; + let color; + if (isSelection && (isStart || isEnd)) color = theme.colors.primary; + else if (isSelection) color = theme.colors.danger; + else color = theme.colors.textDisabled; + markedDates[range[i]] = { + startingDay: isStart, + endingDay: isEnd, + color, + }; + } + return markedDates; }