/*
* 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 React, { useCallback, useRef, useState } from 'react';
import {
Button,
Caption,
Card,
Headline,
Subheading,
useTheme,
} 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 LoadingConfirmDialog from '../../../components/Dialogs/LoadingConfirmDialog';
import ErrorDialog from '../../../components/Dialogs/ErrorDialog';
import {
generateMarkedDates,
getFirstEquipmentAvailability,
getISODate,
getRelativeDateString,
getValidRange,
isEquipmentAvailable,
} from '../../../utils/EquipmentBooking';
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
import { MainStackParamsList } from '../../../navigation/MainNavigator';
import GENERAL_STYLES from '../../../constants/Styles';
import { ApiRejectType } from '../../../utils/WebData';
import { REQUEST_STATUS } from '../../../utils/Requests';
import { useFocusEffect } from '@react-navigation/core';
import { useNavigation } from '@react-navigation/native';
import { useAuthenticatedRequest } from '../../../context/loginContext';
type Props = StackScreenProps;
export type MarkedDatesObjectType = {
[key: string]: PeriodMarking;
};
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,
},
});
function EquipmentRentScreen(props: Props) {
const theme = useTheme();
const navigation = useNavigation>();
const [currentError, setCurrentError] = useState({
status: REQUEST_STATUS.SUCCESS,
});
const [markedDates, setMarkedDates] = useState({});
const [dialogVisible, setDialogVisible] = useState(false);
const item = props.route.params.item;
const bookedDates = useRef>([]);
const canBookEquipment = useRef(false);
const bookRef = useRef(null);
let lockedDates: {
[key: string]: PeriodMarking;
} = {};
if (item) {
item.booked_at.forEach((date: { begin: string; end: string }) => {
const range = getValidRange(
new Date(date.begin),
new Date(date.end),
null
);
lockedDates = {
...lockedDates,
...generateMarkedDates(false, theme, range),
};
});
}
useFocusEffect(
useCallback(() => {
BackHandler.addEventListener(
'hardwareBackPress',
onBackButtonPressAndroid
);
return () => {
BackHandler.removeEventListener(
'hardwareBackPress',
onBackButtonPressAndroid
);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
);
/**
* Overrides default android back button behaviour to deselect date if any is selected.
*
* @return {boolean}
*/
const onBackButtonPressAndroid = (): boolean => {
if (bookedDates.current.length > 0) {
resetSelection();
updateMarkedSelection();
return true;
}
return false;
};
const showDialog = () => setDialogVisible(true);
const onDialogDismiss = () => setDialogVisible(false);
const onErrorDialogDismiss = () =>
setCurrentError({ status: REQUEST_STATUS.SUCCESS });
const getBookStartDate = (): Date | null => {
return bookedDates.current.length > 0
? new Date(bookedDates.current[0])
: null;
};
const getBookEndDate = (): Date | null => {
const { length } = bookedDates.current;
return length > 0 ? new Date(bookedDates.current[length - 1]) : null;
};
const start = getBookStartDate();
const end = getBookEndDate();
const request = useAuthenticatedRequest(
'location/booking',
item && start && end
? {
device: item.id,
begin: getISODate(start),
end: getISODate(end),
}
: undefined
);
/**
* 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}
*/
const onDialogAccept = (): Promise => {
return new Promise((resolve: () => void) => {
if (item != null && start != null && end != null) {
request()
.then(() => {
onDialogDismiss();
navigation.replace('equipment-confirm', {
item: item,
dates: [getISODate(start), getISODate(end)],
});
resolve();
})
.catch((error: ApiRejectType) => {
onDialogDismiss();
setCurrentError(error);
resolve();
});
} else {
onDialogDismiss();
resolve();
}
});
};
/**
* Selects a new date on the calendar.
* If both start and end dates are already selected, unselect all.
*
* @param day The day selected
*/
const selectNewDate = (day: {
dateString: string;
day: number;
month: number;
timestamp: number;
year: number;
}) => {
const selected = new Date(day.dateString);
if (!lockedDates[day.dateString] != null) {
if (start === null) {
updateSelectionRange(selected, selected);
enableBooking();
} else if (start.getTime() === selected.getTime()) {
resetSelection();
} else if (bookedDates.current.length === 1) {
updateSelectionRange(start, selected);
enableBooking();
} else {
resetSelection();
}
updateMarkedSelection();
}
};
const showBookButton = () => {
if (bookRef.current && bookRef.current.fadeInUp) {
bookRef.current.fadeInUp(500);
}
};
const hideBookButton = () => {
if (bookRef.current && bookRef.current.fadeOutDown) {
bookRef.current.fadeOutDown(500);
}
};
const enableBooking = () => {
if (!canBookEquipment.current) {
showBookButton();
canBookEquipment.current = true;
}
};
const resetSelection = () => {
if (canBookEquipment.current) {
hideBookButton();
}
canBookEquipment.current = false;
bookedDates.current = [];
};
const updateSelectionRange = (s: Date, e: Date) => {
if (item) {
bookedDates.current = getValidRange(s, e, item);
} else {
bookedDates.current = [];
}
};
const updateMarkedSelection = () => {
setMarkedDates(generateMarkedDates(true, theme, bookedDates.current));
};
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) {
const isAvailable = isEquipmentAvailable(item);
const firstAvailability = getFirstEquipmentAvailability(item);
return (
{item.name}
({i18n.t('screens.equipment.bail', { cost: item.caution })})
{subHeadingText}
);
}
return null;
}
export default EquipmentRentScreen;