Improve equipment booking components to match linter

This commit is contained in:
Arnaud Vergnet 2020-08-04 10:57:19 +02:00
parent 70365136ac
commit 11b5f2ac71
5 changed files with 871 additions and 863 deletions

View file

@ -2,46 +2,48 @@
import * as React from 'react'; import * as React from 'react';
import {Avatar, List, withTheme} from 'react-native-paper'; import {Avatar, List, withTheme} from 'react-native-paper';
import type {CustomTheme} from "../../../managers/ThemeManager"; import i18n from 'i18n-js';
import type {Device} from "../../../screens/Amicale/Equipment/EquipmentListScreen"; import {StackNavigationProp} from '@react-navigation/stack';
import i18n from "i18n-js"; import type {CustomTheme} from '../../../managers/ThemeManager';
import type {DeviceType} from '../../../screens/Amicale/Equipment/EquipmentListScreen';
import { import {
getFirstEquipmentAvailability, getFirstEquipmentAvailability,
getRelativeDateString, getRelativeDateString,
isEquipmentAvailable isEquipmentAvailable,
} from "../../../utils/EquipmentBooking"; } from '../../../utils/EquipmentBooking';
import {StackNavigationProp} from "@react-navigation/stack";
type Props = { type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
userDeviceRentDates: [string, string], userDeviceRentDates: [string, string],
item: Device, item: DeviceType,
height: number, height: number,
theme: CustomTheme, theme: CustomTheme,
};
class EquipmentListItem extends React.Component<PropsType> {
shouldComponentUpdate(nextProps: PropsType): boolean {
const {userDeviceRentDates} = this.props;
return nextProps.userDeviceRentDates !== userDeviceRentDates;
} }
class EquipmentListItem extends React.Component<Props> { render(): React.Node {
const {item, userDeviceRentDates, navigation, height, theme} = this.props;
shouldComponentUpdate(nextProps: Props): boolean {
return nextProps.userDeviceRentDates !== this.props.userDeviceRentDates;
}
render() {
const colors = this.props.theme.colors;
const item = this.props.item;
const userDeviceRentDates = this.props.userDeviceRentDates;
const isRented = userDeviceRentDates != null; const isRented = userDeviceRentDates != null;
const isAvailable = isEquipmentAvailable(item); const isAvailable = isEquipmentAvailable(item);
const firstAvailability = getFirstEquipmentAvailability(item); const firstAvailability = getFirstEquipmentAvailability(item);
let onPress; let onPress;
if (isRented) if (isRented)
onPress = () => this.props.navigation.navigate("equipment-confirm", { onPress = () => {
item: item, navigation.navigate('equipment-confirm', {
dates: userDeviceRentDates item,
dates: userDeviceRentDates,
}); });
};
else else
onPress = () => this.props.navigation.navigate("equipment-rent", {item: item}); onPress = () => {
navigation.navigate('equipment-rent', {item});
};
let description; let description;
if (isRented) { if (isRented) {
@ -50,58 +52,57 @@ class EquipmentListItem extends React.Component<Props> {
if (start.getTime() !== end.getTime()) if (start.getTime() !== end.getTime())
description = i18n.t('screens.equipment.bookingPeriod', { description = i18n.t('screens.equipment.bookingPeriod', {
begin: getRelativeDateString(start), begin: getRelativeDateString(start),
end: getRelativeDateString(end) end: getRelativeDateString(end),
}); });
else else
description = i18n.t('screens.equipment.bookingDay', { description = i18n.t('screens.equipment.bookingDay', {
date: getRelativeDateString(start) date: getRelativeDateString(start),
}); });
} else if (isAvailable) } else if (isAvailable)
description = i18n.t('screens.equipment.bail', {cost: item.caution}); description = i18n.t('screens.equipment.bail', {cost: item.caution});
else else
description = i18n.t('screens.equipment.available', {date: getRelativeDateString(firstAvailability)}); description = i18n.t('screens.equipment.available', {
date: getRelativeDateString(firstAvailability),
});
let icon; let icon;
if (isRented) if (isRented) icon = 'bookmark-check';
icon = "bookmark-check"; else if (isAvailable) icon = 'check-circle-outline';
else if (isAvailable) else icon = 'update';
icon = "check-circle-outline";
else
icon = "update";
let color; let color;
if (isRented) if (isRented) color = theme.colors.warning;
color = colors.warning; else if (isAvailable) color = theme.colors.success;
else if (isAvailable) else color = theme.colors.primary;
color = colors.success;
else
color = colors.primary;
return ( return (
<List.Item <List.Item
title={item.name} title={item.name}
description={description} description={description}
onPress={onPress} onPress={onPress}
left={(props) => <Avatar.Icon left={({size}: {size: number}): React.Node => (
{...props} <Avatar.Icon
size={size}
style={{ style={{
backgroundColor: 'transparent', backgroundColor: 'transparent',
}} }}
icon={icon} icon={icon}
color={color} color={color}
/>} />
right={(props) => <Avatar.Icon )}
{...props} right={(): React.Node => (
<Avatar.Icon
style={{ style={{
marginTop: 'auto', marginTop: 'auto',
marginBottom: 'auto', marginBottom: 'auto',
backgroundColor: 'transparent', backgroundColor: 'transparent',
}} }}
size={48} size={48}
icon={"chevron-right"} icon="chevron-right"
/>} />
)}
style={{ style={{
height: this.props.height, height,
justifyContent: 'center', justifyContent: 'center',
}} }}
/> />

View file

@ -1,68 +1,79 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {Button, Caption, Card, Headline, Paragraph, withTheme} from 'react-native-paper'; import {
import {StackNavigationProp} from "@react-navigation/stack"; Button,
import type {CustomTheme} from "../../../managers/ThemeManager"; Caption,
import type {Device} from "./EquipmentListScreen"; Card,
import {View} from "react-native"; Headline,
import i18n from "i18n-js"; Paragraph,
import {getRelativeDateString} from "../../../utils/EquipmentBooking"; withTheme,
import CollapsibleScrollView from "../../../components/Collapsible/CollapsibleScrollView"; } 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 = { type PropsType = {
navigation: StackNavigationProp,
route: { route: {
params?: { params?: {
item?: Device, item?: DeviceType,
dates: [string, string] dates: [string, string],
}, },
}, },
theme: CustomTheme, theme: CustomTheme,
} };
class EquipmentConfirmScreen extends React.Component<PropsType> {
item: DeviceType | null;
class EquipmentConfirmScreen extends React.Component<Props> {
item: Device | null;
dates: [string, string] | null; dates: [string, string] | null;
constructor(props: Props) { constructor(props: PropsType) {
super(props); super(props);
if (this.props.route.params != null) { if (props.route.params != null) {
if (this.props.route.params.item != null) if (props.route.params.item != null) this.item = props.route.params.item;
this.item = this.props.route.params.item; else this.item = null;
else if (props.route.params.dates != null)
this.item = null; this.dates = props.route.params.dates;
if (this.props.route.params.dates != null) else this.dates = null;
this.dates = this.props.route.params.dates;
else
this.dates = null;
} }
} }
render() { render(): React.Node {
const item = this.item; const {item, dates, props} = this;
const dates = this.dates;
if (item != null && dates != null) { if (item != null && dates != null) {
const start = new Date(dates[0]); const start = new Date(dates[0]);
const end = new Date(dates[1]); 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 ( return (
<CollapsibleScrollView> <CollapsibleScrollView>
<Card style={{margin: 5}}> <Card style={{margin: 5}}>
<Card.Content> <Card.Content>
<View style={{flex: 1}}> <View style={{flex: 1}}>
<View style={{ <View
marginLeft: "auto", style={{
marginRight: "auto", marginLeft: 'auto',
flexDirection: "row", marginRight: 'auto',
flexWrap: "wrap", flexDirection: 'row',
flexWrap: 'wrap',
}}> }}>
<Headline style={{textAlign: "center"}}> <Headline style={{textAlign: 'center'}}>{item.name}</Headline>
{item.name} <Caption
</Headline> style={{
<Caption style={{ textAlign: 'center',
textAlign: "center",
lineHeight: 35, lineHeight: 35,
marginLeft: 10, marginLeft: 10,
}}> }}>
@ -71,35 +82,21 @@ class EquipmentConfirmScreen extends React.Component<Props> {
</View> </View>
</View> </View>
<Button <Button
icon={"check-circle-outline"} icon="check-circle-outline"
color={this.props.theme.colors.success} color={props.theme.colors.success}
mode="text" mode="text">
> {buttonText}
{
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)
})
}
</Button> </Button>
<Paragraph style={{textAlign: "center"}}> <Paragraph style={{textAlign: 'center'}}>
{i18n.t("screens.equipment.bookingConfirmedMessage")} {i18n.t('screens.equipment.bookingConfirmedMessage')}
</Paragraph> </Paragraph>
</Card.Content> </Card.Content>
</Card> </Card>
</CollapsibleScrollView> </CollapsibleScrollView>
); );
} else
return null;
} }
return null;
}
} }
export default withTheme(EquipmentConfirmScreen); export default withTheme(EquipmentConfirmScreen);

View file

@ -1,61 +1,62 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {View} from "react-native"; import {View} from 'react-native';
import {Button, withTheme} from 'react-native-paper'; import {Button, withTheme} from 'react-native-paper';
import AuthenticatedScreen from "../../../components/Amicale/AuthenticatedScreen"; import {StackNavigationProp} from '@react-navigation/stack';
import {StackNavigationProp} from "@react-navigation/stack"; import i18n from 'i18n-js';
import type {CustomTheme} from "../../../managers/ThemeManager"; import AuthenticatedScreen from '../../../components/Amicale/AuthenticatedScreen';
import i18n from "i18n-js"; import type {ClubType} from '../Clubs/ClubListScreen';
import type {club} from "../Clubs/ClubListScreen"; import EquipmentListItem from '../../../components/Lists/Equipment/EquipmentListItem';
import EquipmentListItem from "../../../components/Lists/Equipment/EquipmentListItem"; import MascotPopup from '../../../components/Mascot/MascotPopup';
import MascotPopup from "../../../components/Mascot/MascotPopup"; import {MASCOT_STYLE} from '../../../components/Mascot/Mascot';
import {MASCOT_STYLE} from "../../../components/Mascot/Mascot"; import AsyncStorageManager from '../../../managers/AsyncStorageManager';
import AsyncStorageManager from "../../../managers/AsyncStorageManager"; import CollapsibleFlatList from '../../../components/Collapsible/CollapsibleFlatList';
import CollapsibleFlatList from "../../../components/Collapsible/CollapsibleFlatList"; import type {ApiGenericDataType} from '../../../utils/WebData';
type Props = { type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
theme: CustomTheme, };
}
type State = { type StateType = {
mascotDialogVisible: boolean, mascotDialogVisible: boolean,
} };
export type Device = { export type DeviceType = {
id: number, id: number,
name: string, name: string,
caution: number, caution: number,
booked_at: Array<{begin: string, end: string}>, booked_at: Array<{begin: string, end: string}>,
}; };
export type RentedDevice = { export type RentedDeviceType = {
device_id: number, device_id: number,
device_name: string, device_name: string,
begin: string, begin: string,
end: string, end: string,
} };
const LIST_ITEM_HEIGHT = 64; const LIST_ITEM_HEIGHT = 64;
class EquipmentListScreen extends React.Component<Props, State> { class EquipmentListScreen extends React.Component<PropsType, StateType> {
data: Array<DeviceType>;
state = { userRents: Array<RentedDeviceType>;
mascotDialogVisible: AsyncStorageManager.getBool(AsyncStorageManager.PREFERENCES.equipmentShowBanner.key),
}
data: Array<Device>;
userRents: Array<RentedDevice>;
authRef: {current: null | AuthenticatedScreen}; authRef: {current: null | AuthenticatedScreen};
canRefresh: boolean; canRefresh: boolean;
constructor(props: Props) { constructor(props: PropsType) {
super(props); super(props);
this.state = {
mascotDialogVisible: AsyncStorageManager.getBool(
AsyncStorageManager.PREFERENCES.equipmentShowBanner.key,
),
};
this.canRefresh = false; this.canRefresh = false;
this.authRef = React.createRef(); this.authRef = React.createRef();
this.props.navigation.addListener('focus', this.onScreenFocus); props.navigation.addListener('focus', this.onScreenFocus);
} }
onScreenFocus = () => { onScreenFocus = () => {
@ -64,25 +65,25 @@ class EquipmentListScreen extends React.Component<Props, State> {
this.canRefresh = true; this.canRefresh = true;
}; };
getRenderItem = ({item}: { item: Device }) => { getRenderItem = ({item}: {item: DeviceType}): React.Node => {
const {navigation} = this.props;
return ( return (
<EquipmentListItem <EquipmentListItem
navigation={this.props.navigation} navigation={navigation}
item={item} item={item}
userDeviceRentDates={this.getUserDeviceRentDates(item)} userDeviceRentDates={this.getUserDeviceRentDates(item)}
height={LIST_ITEM_HEIGHT}/> height={LIST_ITEM_HEIGHT}
/>
); );
}; };
getUserDeviceRentDates(item: Device) { getUserDeviceRentDates(item: DeviceType): [number, number] | null {
let dates = null; let dates = null;
for (let i = 0; i < this.userRents.length; i++) { this.userRents.forEach((device: RentedDeviceType) => {
let device = this.userRents[i];
if (item.id === device.device_id) { if (item.id === device.device_id) {
dates = [device.begin, device.end]; dates = [device.begin, device.end];
break;
}
} }
});
return dates; return dates;
} }
@ -91,28 +92,29 @@ class EquipmentListScreen extends React.Component<Props, State> {
* *
* @returns {*} * @returns {*}
*/ */
getListHeader() { getListHeader(): React.Node {
return ( return (
<View style={{ <View
width: "100%", style={{
width: '100%',
marginTop: 10, marginTop: 10,
marginBottom: 10, marginBottom: 10,
}}> }}>
<Button <Button
mode={"contained"} mode="contained"
icon={"help-circle"} icon="help-circle"
onPress={this.showMascotDialog} onPress={this.showMascotDialog}
style={{ style={{
marginRight: "auto", marginRight: 'auto',
marginLeft: "auto", marginLeft: 'auto',
}}> }}>
{i18n.t("screens.equipment.mascotDialog.title")} {i18n.t('screens.equipment.mascotDialog.title')}
</Button> </Button>
</View> </View>
); );
} }
keyExtractor = (item: club) => item.id.toString(); keyExtractor = (item: ClubType): string => item.id.toString();
/** /**
* Gets the main screen component with the fetched data * Gets the main screen component with the fetched data
@ -120,16 +122,14 @@ class EquipmentListScreen extends React.Component<Props, State> {
* @param data The data fetched from the server * @param data The data fetched from the server
* @returns {*} * @returns {*}
*/ */
getScreen = (data: Array<{ [key: string]: any } | null>) => { getScreen = (data: Array<ApiGenericDataType | null>): React.Node => {
if (data[0] != null) { if (data[0] != null) {
const fetchedData = data[0]; const fetchedData = data[0];
if (fetchedData != null) if (fetchedData != null) this.data = fetchedData.devices;
this.data = fetchedData["devices"];
} }
if (data[1] != null) { if (data[1] != null) {
const fetchedData = data[1]; const fetchedData = data[1];
if (fetchedData != null) if (fetchedData != null) this.userRents = fetchedData.locations;
this.userRents = fetchedData["locations"];
} }
return ( return (
<CollapsibleFlatList <CollapsibleFlatList
@ -138,23 +138,27 @@ class EquipmentListScreen extends React.Component<Props, State> {
ListHeaderComponent={this.getListHeader()} ListHeaderComponent={this.getListHeader()}
data={this.data} data={this.data}
/> />
) );
}; };
showMascotDialog = () => { showMascotDialog = () => {
this.setState({mascotDialogVisible: true}) this.setState({mascotDialogVisible: true});
}; };
hideMascotDialog = () => { hideMascotDialog = () => {
AsyncStorageManager.set(AsyncStorageManager.PREFERENCES.equipmentShowBanner.key, false); AsyncStorageManager.set(
this.setState({mascotDialogVisible: false}) AsyncStorageManager.PREFERENCES.equipmentShowBanner.key,
false,
);
this.setState({mascotDialogVisible: false});
}; };
render() { render(): React.Node {
const {props, state} = this;
return ( return (
<View style={{flex: 1}}> <View style={{flex: 1}}>
<AuthenticatedScreen <AuthenticatedScreen
{...this.props} navigation={props.navigation}
ref={this.authRef} ref={this.authRef}
requests={[ requests={[
{ {
@ -166,22 +170,22 @@ class EquipmentListScreen extends React.Component<Props, State> {
link: 'location/my', link: 'location/my',
params: {}, params: {},
mandatory: false, mandatory: false,
} },
]} ]}
renderFunction={this.getScreen} renderFunction={this.getScreen}
/> />
<MascotPopup <MascotPopup
visible={this.state.mascotDialogVisible} visible={state.mascotDialogVisible}
title={i18n.t("screens.equipment.mascotDialog.title")} title={i18n.t('screens.equipment.mascotDialog.title')}
message={i18n.t("screens.equipment.mascotDialog.message")} message={i18n.t('screens.equipment.mascotDialog.message')}
icon={"vote"} icon="vote"
buttons={{ buttons={{
action: null, action: null,
cancel: { cancel: {
message: i18n.t("screens.equipment.mascotDialog.button"), message: i18n.t('screens.equipment.mascotDialog.button'),
icon: "check", icon: 'check',
onPress: this.hideMascotDialog, onPress: this.hideMascotDialog,
} },
}} }}
emotion={MASCOT_STYLE.WINK} emotion={MASCOT_STYLE.WINK}
/> />

View file

@ -1,111 +1,118 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {Button, Caption, Card, Headline, Subheading, withTheme} from 'react-native-paper'; import {
import {StackNavigationProp} from "@react-navigation/stack"; Button,
import type {CustomTheme} from "../../../managers/ThemeManager"; Caption,
import type {Device} from "./EquipmentListScreen"; Card,
import {BackHandler, View} from "react-native"; Headline,
import * as Animatable from "react-native-animatable"; Subheading,
import i18n from "i18n-js"; withTheme,
import {CalendarList} from "react-native-calendars"; } from 'react-native-paper';
import LoadingConfirmDialog from "../../../components/Dialogs/LoadingConfirmDialog"; import {StackNavigationProp} from '@react-navigation/stack';
import ErrorDialog from "../../../components/Dialogs/ErrorDialog"; 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 { import {
generateMarkedDates, generateMarkedDates,
getFirstEquipmentAvailability, getFirstEquipmentAvailability,
getISODate, getISODate,
getRelativeDateString, getRelativeDateString,
getValidRange, getValidRange,
isEquipmentAvailable isEquipmentAvailable,
} from "../../../utils/EquipmentBooking"; } from '../../../utils/EquipmentBooking';
import ConnectionManager from "../../../managers/ConnectionManager"; import ConnectionManager from '../../../managers/ConnectionManager';
import CollapsibleScrollView from "../../../components/Collapsible/CollapsibleScrollView"; import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
type Props = { type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
route: { route: {
params?: { params?: {
item?: Device, item?: DeviceType,
}, },
}, },
theme: CustomTheme, theme: CustomTheme,
} };
type State = { export type MarkedDatesObjectType = {
[key: string]: {startingDay: boolean, endingDay: boolean, color: string},
};
type StateType = {
dialogVisible: boolean, dialogVisible: boolean,
errorDialogVisible: boolean, errorDialogVisible: boolean,
markedDates: { [key: string]: { startingDay: boolean, endingDay: boolean, color: string } }, markedDates: MarkedDatesObjectType,
currentError: number, currentError: number,
} };
class EquipmentRentScreen extends React.Component<Props, State> { class EquipmentRentScreen extends React.Component<PropsType, StateType> {
item: DeviceType | null;
state = { bookedDates: Array<string>;
bookRef: {current: null | Animatable.View};
canBookEquipment: boolean;
lockedDates: {
[key: string]: {startingDay: boolean, endingDay: boolean, color: string},
};
constructor(props: PropsType) {
super(props);
this.state = {
dialogVisible: false, dialogVisible: false,
errorDialogVisible: false, errorDialogVisible: false,
markedDates: {}, markedDates: {},
currentError: 0, currentError: 0,
} };
item: Device | null;
bookedDates: Array<string>;
bookRef: { current: null | Animatable.View }
canBookEquipment: boolean;
lockedDates: { [key: string]: { startingDay: boolean, endingDay: boolean, color: string } }
constructor(props: Props) {
super(props);
this.resetSelection(); this.resetSelection();
this.bookRef = React.createRef(); this.bookRef = React.createRef();
this.canBookEquipment = false; this.canBookEquipment = false;
this.bookedDates = []; this.bookedDates = [];
if (this.props.route.params != null) { if (props.route.params != null) {
if (this.props.route.params.item != null) if (props.route.params.item != null) this.item = props.route.params.item;
this.item = this.props.route.params.item; else this.item = null;
else
this.item = null;
} }
const item = this.item; const {item} = this;
if (item != null) { if (item != null) {
this.lockedDates = {}; this.lockedDates = {};
for (let i = 0; i < item.booked_at.length; i++) { item.booked_at.forEach((date: {begin: string, end: string}) => {
const range = getValidRange(new Date(item.booked_at[i].begin), new Date(item.booked_at[i].end), null); const range = getValidRange(
new Date(date.begin),
new Date(date.end),
null,
);
this.lockedDates = { this.lockedDates = {
...this.lockedDates, ...this.lockedDates,
...generateMarkedDates( ...generateMarkedDates(false, props.theme, range),
false,
this.props.theme,
range
)
}; };
});
} }
} }
}
/** /**
* Captures focus and blur events to hook on android back button * Captures focus and blur events to hook on android back button
*/ */
componentDidMount() { componentDidMount() {
this.props.navigation.addListener( const {navigation} = this.props;
'focus', navigation.addListener('focus', () => {
() =>
BackHandler.addEventListener( BackHandler.addEventListener(
'hardwareBackPress', 'hardwareBackPress',
this.onBackButtonPressAndroid this.onBackButtonPressAndroid,
)
); );
this.props.navigation.addListener( });
'blur', navigation.addListener('blur', () => {
() =>
BackHandler.removeEventListener( BackHandler.removeEventListener(
'hardwareBackPress', 'hardwareBackPress',
this.onBackButtonPressAndroid this.onBackButtonPressAndroid,
)
); );
});
} }
/** /**
@ -113,26 +120,88 @@ class EquipmentRentScreen extends React.Component<Props, State> {
* *
* @return {boolean} * @return {boolean}
*/ */
onBackButtonPressAndroid = () => { onBackButtonPressAndroid = (): boolean => {
if (this.bookedDates.length > 0) { if (this.bookedDates.length > 0) {
this.resetSelection(); this.resetSelection();
this.updateMarkedSelection(); this.updateMarkedSelection();
return true; return true;
} else }
return false; 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<void>}
*/
onDialogAccept = (): Promise<void> => {
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. * Selects a new date on the calendar.
* If both start and end dates are already selected, unselect all. * If both start and end dates are already selected, unselect all.
* *
* @param day The day selected * @param day The day selected
*/ */
selectNewDate = (day: { dateString: string, day: number, month: number, timestamp: number, year: number }) => { selectNewDate = (day: {
dateString: string,
day: number,
month: number,
timestamp: number,
year: number,
}) => {
const selected = new Date(day.dateString); const selected = new Date(day.dateString);
const start = this.getBookStartDate(); const start = this.getBookStartDate();
if (!(this.lockedDates.hasOwnProperty(day.dateString))) { if (!this.lockedDates[day.dateString] != null) {
if (start === null) { if (start === null) {
this.updateSelectionRange(selected, selected); this.updateSelectionRange(selected, selected);
this.enableBooking(); this.enableBooking();
@ -141,39 +210,21 @@ class EquipmentRentScreen extends React.Component<Props, State> {
} else if (this.bookedDates.length === 1) { } else if (this.bookedDates.length === 1) {
this.updateSelectionRange(start, selected); this.updateSelectionRange(start, selected);
this.enableBooking(); this.enableBooking();
} else } else this.resetSelection();
this.resetSelection();
this.updateMarkedSelection(); this.updateMarkedSelection();
} }
} };
updateSelectionRange(start: Date, end: Date) { showErrorDialog = (error: number) => {
this.bookedDates = getValidRange(start, end, this.item);
}
updateMarkedSelection() {
this.setState({ this.setState({
markedDates: generateMarkedDates( errorDialogVisible: true,
true, currentError: error,
this.props.theme,
this.bookedDates
),
}); });
} };
enableBooking() { showDialog = () => {
if (!this.canBookEquipment) { this.setState({dialogVisible: true});
this.showBookButton(); };
this.canBookEquipment = true;
}
}
resetSelection() {
if (this.canBookEquipment)
this.hideBookButton();
this.canBookEquipment = false;
this.bookedDates = [];
}
/** /**
* Shows the book button by plying a fade animation * Shows the book button by plying a fade animation
@ -193,84 +244,45 @@ class EquipmentRentScreen extends React.Component<Props, State> {
} }
} }
showDialog = () => { enableBooking() {
this.setState({dialogVisible: true}); if (!this.canBookEquipment) {
this.showBookButton();
this.canBookEquipment = true;
}
} }
showErrorDialog = (error: number) => { 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({ this.setState({
errorDialogVisible: true, markedDates: generateMarkedDates(true, theme, this.bookedDates),
currentError: error,
}); });
} }
onDialogDismiss = () => { render(): React.Node {
this.setState({dialogVisible: false}); const {item, props, state} = this;
}
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<R>}
*/
onDialogAccept = () => {
return new Promise((resolve) => {
const item = this.item;
const start = this.getBookStartDate(); const start = this.getBookStartDate();
const end = this.getBookEndDate(); const end = this.getBookEndDate();
if (item != null && start != null && end != null) { let subHeadingText;
console.log({ if (start == null) subHeadingText = i18n.t('screens.equipment.booking');
"device": item.id, else if (end != null && start.getTime() !== end.getTime())
"begin": getISODate(start), subHeadingText = i18n.t('screens.equipment.bookingPeriod', {
"end": getISODate(end), begin: getRelativeDateString(start),
}) end: getRelativeDateString(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(); else
}) i18n.t('screens.equipment.bookingDay', {
.catch((error: number) => { date: getRelativeDateString(start),
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) { if (item != null) {
const isAvailable = isEquipmentAvailable(item); const isAvailable = isEquipmentAvailable(item);
const firstAvailability = getFirstEquipmentAvailability(item); const firstAvailability = getFirstEquipmentAvailability(item);
@ -280,17 +292,19 @@ class EquipmentRentScreen extends React.Component<Props, State> {
<Card style={{margin: 5}}> <Card style={{margin: 5}}>
<Card.Content> <Card.Content>
<View style={{flex: 1}}> <View style={{flex: 1}}>
<View style={{ <View
marginLeft: "auto", style={{
marginRight: "auto", marginLeft: 'auto',
flexDirection: "row", marginRight: 'auto',
flexWrap: "wrap", flexDirection: 'row',
flexWrap: 'wrap',
}}> }}>
<Headline style={{textAlign: "center"}}> <Headline style={{textAlign: 'center'}}>
{item.name} {item.name}
</Headline> </Headline>
<Caption style={{ <Caption
textAlign: "center", style={{
textAlign: 'center',
lineHeight: 35, lineHeight: 35,
marginLeft: 10, marginLeft: 10,
}}> }}>
@ -300,30 +314,24 @@ class EquipmentRentScreen extends React.Component<Props, State> {
</View> </View>
<Button <Button
icon={isAvailable ? "check-circle-outline" : "update"} icon={isAvailable ? 'check-circle-outline' : 'update'}
color={isAvailable ? this.props.theme.colors.success : this.props.theme.colors.primary} color={
mode="text" isAvailable
> ? props.theme.colors.success
{i18n.t('screens.equipment.available', {date: getRelativeDateString(firstAvailability)})} : props.theme.colors.primary
</Button>
<Subheading style={{
textAlign: "center",
marginBottom: 10,
minHeight: 50
}}>
{
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)
})
} }
mode="text">
{i18n.t('screens.equipment.available', {
date: getRelativeDateString(firstAvailability),
})}
</Button>
<Subheading
style={{
textAlign: 'center',
marginBottom: 10,
minHeight: 50,
}}>
{subHeadingText}
</Subheading> </Subheading>
</Card.Content> </Card.Content>
</Card> </Card>
@ -335,35 +343,34 @@ class EquipmentRentScreen extends React.Component<Props, State> {
// Max amount of months allowed to scroll to the future. Default = 50 // Max amount of months allowed to scroll to the future. Default = 50
futureScrollRange={3} futureScrollRange={3}
// Enable horizontal scrolling, default = false // Enable horizontal scrolling, default = false
horizontal={true} horizontal
// Enable paging on horizontal, default = false // Enable paging on horizontal, default = false
pagingEnabled={true} pagingEnabled
// Handler which gets executed on day press. Default = undefined // Handler which gets executed on day press. Default = undefined
onDayPress={this.selectNewDate} onDayPress={this.selectNewDate}
// If firstDay=1 week starts from Monday. Note that dayNames and dayNamesShort should still start from Sunday. // If firstDay=1 week starts from Monday. Note that dayNames and dayNamesShort should still start from Sunday.
firstDay={1} firstDay={1}
// Disable all touch events for disabled days. can be override with disableTouchEvent in markedDates // Disable all touch events for disabled days. can be override with disableTouchEvent in markedDates
disableAllTouchEventsForDisabledDays={true} disableAllTouchEventsForDisabledDays
// Hide month navigation arrows. // Hide month navigation arrows.
hideArrows={false} hideArrows={false}
// Date marking style [simple/period/multi-dot/custom]. Default = 'simple' // Date marking style [simple/period/multi-dot/custom]. Default = 'simple'
markingType={'period'} markingType="period"
markedDates={{...this.lockedDates, ...this.state.markedDates}} markedDates={{...this.lockedDates, ...state.markedDates}}
theme={{ theme={{
backgroundColor: this.props.theme.colors.agendaBackgroundColor, backgroundColor: props.theme.colors.agendaBackgroundColor,
calendarBackground: this.props.theme.colors.background, calendarBackground: props.theme.colors.background,
textSectionTitleColor: this.props.theme.colors.agendaDayTextColor, textSectionTitleColor: props.theme.colors.agendaDayTextColor,
selectedDayBackgroundColor: this.props.theme.colors.primary, selectedDayBackgroundColor: props.theme.colors.primary,
selectedDayTextColor: '#ffffff', selectedDayTextColor: '#ffffff',
todayTextColor: this.props.theme.colors.text, todayTextColor: props.theme.colors.text,
dayTextColor: this.props.theme.colors.text, dayTextColor: props.theme.colors.text,
textDisabledColor: this.props.theme.colors.agendaDayTextColor, textDisabledColor: props.theme.colors.agendaDayTextColor,
dotColor: this.props.theme.colors.primary, dotColor: props.theme.colors.primary,
selectedDotColor: '#ffffff', selectedDotColor: '#ffffff',
arrowColor: this.props.theme.colors.primary, arrowColor: props.theme.colors.primary,
monthTextColor: this.props.theme.colors.text, monthTextColor: props.theme.colors.text,
indicatorColor: this.props.theme.colors.primary, indicatorColor: props.theme.colors.primary,
textDayFontFamily: 'monospace', textDayFontFamily: 'monospace',
textMonthFontFamily: 'monospace', textMonthFontFamily: 'monospace',
textDayHeaderFontFamily: 'monospace', textDayHeaderFontFamily: 'monospace',
@ -379,15 +386,14 @@ class EquipmentRentScreen extends React.Component<Props, State> {
height: 34, height: 34,
width: 34, width: 34,
alignItems: 'center', alignItems: 'center',
},
} },
}
}} }}
style={{marginBottom: 50}} style={{marginBottom: 50}}
/> />
</CollapsibleScrollView> </CollapsibleScrollView>
<LoadingConfirmDialog <LoadingConfirmDialog
visible={this.state.dialogVisible} visible={state.dialogVisible}
onDismiss={this.onDialogDismiss} onDismiss={this.onDialogDismiss}
onAccept={this.onDialogAccept} onAccept={this.onDialogAccept}
title={i18n.t('screens.equipment.dialogTitle')} title={i18n.t('screens.equipment.dialogTitle')}
@ -396,46 +402,40 @@ class EquipmentRentScreen extends React.Component<Props, State> {
/> />
<ErrorDialog <ErrorDialog
visible={this.state.errorDialogVisible} visible={state.errorDialogVisible}
onDismiss={this.onErrorDialogDismiss} onDismiss={this.onErrorDialogDismiss}
errorCode={this.state.currentError} errorCode={state.currentError}
/> />
<Animatable.View <Animatable.View
ref={this.bookRef} ref={this.bookRef}
style={{ style={{
position: "absolute", position: 'absolute',
bottom: 0, bottom: 0,
left: 0, left: 0,
width: "100%", width: '100%',
flex: 1, flex: 1,
transform: [ transform: [{translateY: 100}],
{translateY: 100},
]
}}> }}>
<Button <Button
icon="bookmark-check" icon="bookmark-check"
mode="contained" mode="contained"
onPress={this.showDialog} onPress={this.showDialog}
style={{ style={{
width: "80%", width: '80%',
flex: 1, flex: 1,
marginLeft: "auto", marginLeft: 'auto',
marginRight: "auto", marginRight: 'auto',
marginBottom: 20, marginBottom: 20,
borderRadius: 10 borderRadius: 10,
}} }}>
>
{i18n.t('screens.equipment.bookButton')} {i18n.t('screens.equipment.bookButton')}
</Button> </Button>
</Animatable.View> </Animatable.View>
</View> </View>
);
) }
} else return null;
return <View/>;
} }
} }
export default withTheme(EquipmentRentScreen); export default withTheme(EquipmentRentScreen);

View file

@ -1,17 +1,18 @@
// @flow // @flow
import type {Device} from "../screens/Amicale/Equipment/EquipmentListScreen"; import i18n from 'i18n-js';
import i18n from "i18n-js"; import type {DeviceType} from '../screens/Amicale/Equipment/EquipmentListScreen';
import DateManager from "../managers/DateManager"; import DateManager from '../managers/DateManager';
import type {CustomTheme} from "../managers/ThemeManager"; import type {CustomTheme} from '../managers/ThemeManager';
import type {MarkedDatesObjectType} from '../screens/Amicale/Equipment/EquipmentRentScreen';
/** /**
* Gets the current day at midnight * Gets the current day at midnight
* *
* @returns {Date} * @returns {Date}
*/ */
export function getCurrentDay() { export function getCurrentDay(): Date {
let today = new Date(Date.now()); const today = new Date(Date.now());
today.setUTCHours(0, 0, 0, 0); today.setUTCHours(0, 0, 0, 0);
return today; return today;
} }
@ -22,8 +23,8 @@ export function getCurrentDay() {
* @param date The date to recover the ISO format from * @param date The date to recover the ISO format from
* @returns {*} * @returns {*}
*/ */
export function getISODate(date: Date) { export function getISODate(date: Date): string {
return date.toISOString().split("T")[0]; return date.toISOString().split('T')[0];
} }
/** /**
@ -32,17 +33,15 @@ export function getISODate(date: Date) {
* @param item * @param item
* @returns {boolean} * @returns {boolean}
*/ */
export function isEquipmentAvailable(item: Device) { export function isEquipmentAvailable(item: DeviceType): boolean {
let isAvailable = true; let isAvailable = true;
const today = getCurrentDay(); const today = getCurrentDay();
const dates = item.booked_at; const dates = item.booked_at;
for (let i = 0; i < dates.length; i++) { dates.forEach((date: {begin: string, end: string}) => {
const start = new Date(dates[i].begin); const start = new Date(date.begin);
const end = new Date(dates[i].end); const end = new Date(date.end);
isAvailable = today < start || today > end; if (!(today < start || today > end)) isAvailable = false;
if (!isAvailable) });
break;
}
return isAvailable; return isAvailable;
} }
@ -52,16 +51,15 @@ export function isEquipmentAvailable(item: Device) {
* @param item * @param item
* @returns {Date} * @returns {Date}
*/ */
export function getFirstEquipmentAvailability(item: Device) { export function getFirstEquipmentAvailability(item: DeviceType): Date {
let firstAvailability = getCurrentDay(); let firstAvailability = getCurrentDay();
const dates = item.booked_at; const dates = item.booked_at;
for (let i = 0; i < dates.length; i++) { dates.forEach((date: {begin: string, end: string}) => {
const start = new Date(dates[i].begin); const start = new Date(date.begin);
let end = new Date(dates[i].end); const end = new Date(date.end);
end.setDate(end.getDate() + 1); end.setDate(end.getDate() + 1);
if (firstAvailability >= start) if (firstAvailability >= start) firstAvailability = end;
firstAvailability = end; });
}
return firstAvailability; return firstAvailability;
} }
@ -70,7 +68,7 @@ export function getFirstEquipmentAvailability(item: Device) {
* *
* @param date The date to translate * @param date The date to translate
*/ */
export function getRelativeDateString(date: Date) { export function getRelativeDateString(date: Date): string {
const today = getCurrentDay(); const today = getCurrentDay();
const yearDelta = date.getUTCFullYear() - today.getUTCFullYear(); const yearDelta = date.getUTCFullYear() - today.getUTCFullYear();
const monthDelta = date.getUTCMonth() - today.getUTCMonth(); const monthDelta = date.getUTCMonth() - today.getUTCMonth();
@ -80,7 +78,7 @@ export function getRelativeDateString(date: Date) {
translatedString = i18n.t('screens.equipment.otherYear', { translatedString = i18n.t('screens.equipment.otherYear', {
date: date.getDate(), date: date.getDate(),
month: DateManager.getInstance().getMonthsOfYear()[date.getMonth()], month: DateManager.getInstance().getMonthsOfYear()[date.getMonth()],
year: date.getFullYear() year: date.getFullYear(),
}); });
else if (monthDelta > 0) else if (monthDelta > 0)
translatedString = i18n.t('screens.equipment.otherMonth', { translatedString = i18n.t('screens.equipment.otherMonth', {
@ -111,13 +109,17 @@ export function getRelativeDateString(date: Date) {
* @param item Item containing booked dates to look for * @param item Item containing booked dates to look for
* @returns {[string]} * @returns {[string]}
*/ */
export function getValidRange(start: Date, end: Date, item: Device | null) { export function getValidRange(
let direction = start <= end ? 1 : -1; start: Date,
end: Date,
item: DeviceType | null,
): Array<string> {
const direction = start <= end ? 1 : -1;
let limit = new Date(end); let limit = new Date(end);
limit.setDate(limit.getDate() + direction); // Limit is excluded, but we want to include range end limit.setDate(limit.getDate() + direction); // Limit is excluded, but we want to include range end
if (item != null) { if (item != null) {
if (direction === 1) { if (direction === 1) {
for (let i = 0; i < item.booked_at.length; i++) { for (let i = 0; i < item.booked_at.length; i += 1) {
const bookLimit = new Date(item.booked_at[i].begin); const bookLimit = new Date(item.booked_at[i].begin);
if (start < bookLimit && limit > bookLimit) { if (start < bookLimit && limit > bookLimit) {
limit = bookLimit; limit = bookLimit;
@ -125,7 +127,7 @@ export function getValidRange(start: Date, end: Date, item: Device | null) {
} }
} }
} else { } else {
for (let i = item.booked_at.length - 1; i >= 0; i--) { for (let i = item.booked_at.length - 1; i >= 0; i -= 1) {
const bookLimit = new Date(item.booked_at[i].end); const bookLimit = new Date(item.booked_at[i].end);
if (start > bookLimit && limit < bookLimit) { if (start > bookLimit && limit < bookLimit) {
limit = bookLimit; limit = bookLimit;
@ -135,14 +137,14 @@ export function getValidRange(start: Date, end: Date, item: Device | null) {
} }
} }
const validRange = [];
let validRange = []; const date = new Date(start);
let date = new Date(start); while (
while ((direction === 1 && date < limit) || (direction === -1 && date > limit)) { (direction === 1 && date < limit) ||
if (direction === 1) (direction === -1 && date > limit)
validRange.push(getISODate(date)); ) {
else if (direction === 1) validRange.push(getISODate(date));
validRange.unshift(getISODate(date)); else validRange.unshift(getISODate(date));
date.setDate(date.getDate() + direction); date.setDate(date.getDate() + direction);
} }
return validRange; return validRange;
@ -157,19 +159,23 @@ export function getValidRange(start: Date, end: Date, item: Device | null) {
* @param range The range to mark dates for * @param range The range to mark dates for
* @returns {{}} * @returns {{}}
*/ */
export function generateMarkedDates(isSelection: boolean, theme: CustomTheme, range: Array<string>) { export function generateMarkedDates(
let markedDates = {} isSelection: boolean,
for (let i = 0; i < range.length; i++) { theme: CustomTheme,
range: Array<string>,
): MarkedDatesObjectType {
const markedDates = {};
for (let i = 0; i < range.length; i += 1) {
const isStart = i === 0; const isStart = i === 0;
const isEnd = i === range.length - 1; 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]] = { markedDates[range[i]] = {
startingDay: isStart, startingDay: isStart,
endingDay: isEnd, endingDay: isEnd,
color: isSelection color,
? isStart || isEnd
? theme.colors.primary
: theme.colors.danger
: theme.colors.textDisabled
}; };
} }
return markedDates; return markedDates;