123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435 |
- /*
- * 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 <https://www.gnu.org/licenses/>.
- */
-
- 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<MainStackParamsList, 'equipment-rent'>;
-
- 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<StackNavigationProp<any>>();
- const [currentError, setCurrentError] = useState<ApiRejectType>({
- status: REQUEST_STATUS.SUCCESS,
- });
- const [markedDates, setMarkedDates] = useState<MarkedDatesObjectType>({});
- const [dialogVisible, setDialogVisible] = useState(false);
-
- const item = props.route.params.item;
-
- const bookedDates = useRef<Array<string>>([]);
- const canBookEquipment = useRef(false);
-
- const bookRef = useRef<Animatable.View & View>(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<void>}
- */
- const onDialogAccept = (): Promise<void> => {
- 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 (
- <View style={GENERAL_STYLES.flex}>
- <CollapsibleScrollView>
- <Card style={styles.card}>
- <Card.Content>
- <View style={GENERAL_STYLES.flex}>
- <View style={styles.titleContainer}>
- <Headline style={styles.title}>{item.name}</Headline>
- <Caption style={styles.caption}>
- ({i18n.t('screens.equipment.bail', { cost: item.caution })})
- </Caption>
- </View>
- </View>
-
- <Button
- icon={isAvailable ? 'check-circle-outline' : 'update'}
- color={
- isAvailable ? theme.colors.success : theme.colors.primary
- }
- mode="text"
- >
- {i18n.t('screens.equipment.available', {
- date: getRelativeDateString(firstAvailability),
- })}
- </Button>
- <Subheading style={styles.subtitle}>{subHeadingText}</Subheading>
- </Card.Content>
- </Card>
- <CalendarList
- // Minimum date that can be selected, dates before minDate will be grayed out. Default = undefined
- minDate={new Date()}
- // Max amount of months allowed to scroll to the past. Default = 50
- pastScrollRange={0}
- // Max amount of months allowed to scroll to the future. Default = 50
- futureScrollRange={3}
- // Enable horizontal scrolling, default = false
- horizontal
- // Enable paging on horizontal, default = false
- pagingEnabled
- // Handler which gets executed on day press. Default = undefined
- onDayPress={selectNewDate}
- // If firstDay=1 week starts from Monday. Note that dayNames and dayNamesShort should still start from Sunday.
- firstDay={1}
- // Hide month navigation arrows.
- hideArrows={false}
- // Date marking style [simple/period/multi-dot/custom]. Default = 'simple'
- markingType={'period'}
- markedDates={{ ...lockedDates, ...markedDates }}
- theme={{
- 'backgroundColor': theme.colors.agendaBackgroundColor,
- 'calendarBackground': theme.colors.background,
- 'textSectionTitleColor': theme.colors.agendaDayTextColor,
- 'selectedDayBackgroundColor': theme.colors.primary,
- 'selectedDayTextColor': '#ffffff',
- 'todayTextColor': theme.colors.text,
- 'dayTextColor': theme.colors.text,
- 'textDisabledColor': theme.colors.agendaDayTextColor,
- 'dotColor': theme.colors.primary,
- 'selectedDotColor': '#ffffff',
- 'arrowColor': theme.colors.primary,
- 'monthTextColor': theme.colors.text,
- 'indicatorColor': theme.colors.primary,
- 'textDayFontFamily': 'monospace',
- 'textMonthFontFamily': 'monospace',
- 'textDayHeaderFontFamily': 'monospace',
- 'textDayFontWeight': '300',
- 'textMonthFontWeight': 'bold',
- 'textDayHeaderFontWeight': '300',
- 'textDayFontSize': 16,
- 'textMonthFontSize': 16,
- 'textDayHeaderFontSize': 16,
- 'stylesheet.day.period': {
- base: {
- overflow: 'hidden',
- height: 34,
- width: 34,
- alignItems: 'center',
- },
- },
- }}
- style={styles.calendar}
- />
- </CollapsibleScrollView>
- <LoadingConfirmDialog
- visible={dialogVisible}
- onDismiss={onDialogDismiss}
- onAccept={onDialogAccept}
- title={i18n.t('screens.equipment.dialogTitle')}
- titleLoading={i18n.t('screens.equipment.dialogTitleLoading')}
- message={i18n.t('screens.equipment.dialogMessage')}
- />
-
- <ErrorDialog
- visible={currentError.status !== REQUEST_STATUS.SUCCESS}
- onDismiss={onErrorDialogDismiss}
- status={currentError.status}
- code={currentError.code}
- />
- <Animatable.View
- ref={bookRef}
- useNativeDriver
- style={styles.buttonContainer}
- >
- <Button
- icon="bookmark-check"
- mode="contained"
- onPress={showDialog}
- style={styles.button}
- >
- {i18n.t('screens.equipment.bookButton')}
- </Button>
- </Animatable.View>
- </View>
- );
- }
- return null;
- }
-
- export default EquipmentRentScreen;
|