/*
* 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);