Update equipment screens to use TypeScript

This commit is contained in:
Arnaud Vergnet 2020-09-22 19:36:48 +02:00
parent 5977ce257b
commit 67cb96dd03
6 changed files with 196 additions and 196 deletions

View file

@ -30,7 +30,7 @@ import {
type PropsType = { type PropsType = {
navigation: StackNavigationProp<any>; navigation: StackNavigationProp<any>;
userDeviceRentDates: [string, string]; userDeviceRentDates: [string, string] | null;
item: DeviceType; item: DeviceType;
height: number; height: number;
}; };
@ -57,7 +57,7 @@ function EquipmentListItem(props: PropsType) {
} }
let description; let description;
if (isRented) { if (isRented && userDeviceRentDates) {
const start = new Date(userDeviceRentDates[0]); const start = new Date(userDeviceRentDates[0]);
const end = new Date(userDeviceRentDates[1]); const end = new Date(userDeviceRentDates[1]);
if (start.getTime() !== end.getTime()) { if (start.getTime() !== end.getTime()) {

View file

@ -1,121 +0,0 @@
/*
* 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/>.
*/
// @flow
import * as React from 'react';
import {
Button,
Caption,
Card,
Headline,
Paragraph,
withTheme,
} from 'react-native-paper';
import {View} from 'react-native';
import i18n from 'i18n-js';
import type {CustomThemeType} from '../../../managers/ThemeManager';
import type {DeviceType} from './EquipmentListScreen';
import {getRelativeDateString} from '../../../utils/EquipmentBooking';
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
type PropsType = {
route: {
params?: {
item?: DeviceType,
dates: [string, string],
},
},
theme: CustomThemeType,
};
class EquipmentConfirmScreen extends React.Component<PropsType> {
item: DeviceType | null;
dates: [string, string] | 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(): 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 (
<CollapsibleScrollView>
<Card style={{margin: 5}}>
<Card.Content>
<View style={{flex: 1}}>
<View
style={{
marginLeft: 'auto',
marginRight: 'auto',
flexDirection: 'row',
flexWrap: 'wrap',
}}>
<Headline style={{textAlign: 'center'}}>{item.name}</Headline>
<Caption
style={{
textAlign: 'center',
lineHeight: 35,
marginLeft: 10,
}}>
({i18n.t('screens.equipment.bail', {cost: item.caution})})
</Caption>
</View>
</View>
<Button
icon="check-circle-outline"
color={props.theme.colors.success}
mode="text">
{buttonText}
</Button>
<Paragraph style={{textAlign: 'center'}}>
{i18n.t('screens.equipment.bookingConfirmedMessage')}
</Paragraph>
</Card.Content>
</Card>
</CollapsibleScrollView>
);
}
return null;
}
}
export default withTheme(EquipmentConfirmScreen);

View file

@ -0,0 +1,104 @@
/*
* 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 * as React from 'react';
import {
Button,
Caption,
Card,
Headline,
Paragraph,
useTheme,
} from 'react-native-paper';
import {View} from 'react-native';
import i18n from 'i18n-js';
import {getRelativeDateString} from '../../../utils/EquipmentBooking';
import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
import {StackScreenProps} from '@react-navigation/stack';
import {MainStackParamsList} from '../../../navigation/MainNavigator';
type EquipmentConfirmScreenNavigationProp = StackScreenProps<
MainStackParamsList,
'equipment-confirm'
>;
type Props = EquipmentConfirmScreenNavigationProp;
function EquipmentConfirmScreen(props: Props) {
const theme = useTheme();
const item = props.route.params?.item;
const dates = props.route.params?.dates;
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 (
<CollapsibleScrollView>
<Card style={{margin: 5}}>
<Card.Content>
<View style={{flex: 1}}>
<View
style={{
marginLeft: 'auto',
marginRight: 'auto',
flexDirection: 'row',
flexWrap: 'wrap',
}}>
<Headline style={{textAlign: 'center'}}>{item.name}</Headline>
<Caption
style={{
textAlign: 'center',
lineHeight: 35,
marginLeft: 10,
}}>
({i18n.t('screens.equipment.bail', {cost: item.caution})})
</Caption>
</View>
</View>
<Button
icon="check-circle-outline"
color={theme.colors.success}
mode="text">
{buttonText}
</Button>
<Paragraph style={{textAlign: 'center'}}>
{i18n.t('screens.equipment.bookingConfirmedMessage')}
</Paragraph>
</Card.Content>
</Card>
</CollapsibleScrollView>
);
}
return null;
}
export default EquipmentConfirmScreen;

View file

@ -17,42 +17,38 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @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} from 'react-native-paper';
import {StackNavigationProp} from '@react-navigation/stack'; import {StackNavigationProp} from '@react-navigation/stack';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import AuthenticatedScreen from '../../../components/Amicale/AuthenticatedScreen'; import AuthenticatedScreen from '../../../components/Amicale/AuthenticatedScreen';
import type {ClubType} 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 PropsType = { type PropsType = {
navigation: StackNavigationProp, navigation: StackNavigationProp<any>;
}; };
type StateType = { type StateType = {
mascotDialogVisible: boolean, mascotDialogVisible: boolean;
}; };
export type DeviceType = { 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 RentedDeviceType = { 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;
@ -60,12 +56,13 @@ const LIST_ITEM_HEIGHT = 64;
class EquipmentListScreen extends React.Component<PropsType, StateType> { class EquipmentListScreen extends React.Component<PropsType, StateType> {
userRents: null | Array<RentedDeviceType>; userRents: null | Array<RentedDeviceType>;
authRef: {current: null | AuthenticatedScreen}; authRef: {current: null | AuthenticatedScreen<any>};
canRefresh: boolean; canRefresh: boolean;
constructor(props: PropsType) { constructor(props: PropsType) {
super(props); super(props);
this.userRents = null;
this.state = { this.state = {
mascotDialogVisible: AsyncStorageManager.getBool( mascotDialogVisible: AsyncStorageManager.getBool(
AsyncStorageManager.PREFERENCES.equipmentShowMascot.key, AsyncStorageManager.PREFERENCES.equipmentShowMascot.key,
@ -77,12 +74,17 @@ class EquipmentListScreen extends React.Component<PropsType, StateType> {
} }
onScreenFocus = () => { onScreenFocus = () => {
if (this.canRefresh && this.authRef.current != null) if (
this.canRefresh &&
this.authRef.current &&
this.authRef.current.reload
) {
this.authRef.current.reload(); this.authRef.current.reload();
}
this.canRefresh = true; this.canRefresh = true;
}; };
getRenderItem = ({item}: {item: DeviceType}): React.Node => { getRenderItem = ({item}: {item: DeviceType}) => {
const {navigation} = this.props; const {navigation} = this.props;
return ( return (
<EquipmentListItem <EquipmentListItem
@ -94,7 +96,7 @@ class EquipmentListScreen extends React.Component<PropsType, StateType> {
); );
}; };
getUserDeviceRentDates(item: DeviceType): [number, number] | null { getUserDeviceRentDates(item: DeviceType): [string, string] | null {
let dates = null; let dates = null;
if (this.userRents != null) { if (this.userRents != null) {
this.userRents.forEach((device: RentedDeviceType) => { this.userRents.forEach((device: RentedDeviceType) => {
@ -111,7 +113,7 @@ class EquipmentListScreen extends React.Component<PropsType, StateType> {
* *
* @returns {*} * @returns {*}
*/ */
getListHeader(): React.Node { getListHeader() {
return ( return (
<View <View
style={{ style={{
@ -133,7 +135,7 @@ class EquipmentListScreen extends React.Component<PropsType, StateType> {
); );
} }
keyExtractor = (item: ClubType): string => item.id.toString(); keyExtractor = (item: DeviceType): string => item.id.toString();
/** /**
* Gets the main screen component with the fetched data * Gets the main screen component with the fetched data
@ -141,15 +143,27 @@ class EquipmentListScreen extends React.Component<PropsType, StateType> {
* @param data The data fetched from the server * @param data The data fetched from the server
* @returns {*} * @returns {*}
*/ */
getScreen = (data: Array<ApiGenericDataType | null>): React.Node => { getScreen = (
data: Array<
{devices: Array<DeviceType>} | {locations: Array<RentedDeviceType>} | null
>,
) => {
const [allDevices, userRents] = data; const [allDevices, userRents] = data;
if (userRents != null) this.userRents = userRents.locations; if (userRents) {
this.userRents = (userRents as {
locations: Array<RentedDeviceType>;
}).locations;
}
return ( return (
<CollapsibleFlatList <CollapsibleFlatList
keyExtractor={this.keyExtractor} keyExtractor={this.keyExtractor}
renderItem={this.getRenderItem} renderItem={this.getRenderItem}
ListHeaderComponent={this.getListHeader()} ListHeaderComponent={this.getListHeader()}
data={allDevices != null ? allDevices.devices : null} data={
allDevices
? (allDevices as {devices: Array<DeviceType>}).devices
: null
}
/> />
); );
}; };
@ -166,7 +180,7 @@ class EquipmentListScreen extends React.Component<PropsType, StateType> {
this.setState({mascotDialogVisible: false}); this.setState({mascotDialogVisible: false});
}; };
render(): React.Node { render() {
const {props, state} = this; const {props, state} = this;
return ( return (
<View style={{flex: 1}}> <View style={{flex: 1}}>
@ -193,7 +207,6 @@ class EquipmentListScreen extends React.Component<PropsType, StateType> {
message={i18n.t('screens.equipment.mascotDialog.message')} message={i18n.t('screens.equipment.mascotDialog.message')}
icon="vote" icon="vote"
buttons={{ buttons={{
action: null,
cancel: { cancel: {
message: i18n.t('screens.equipment.mascotDialog.button'), message: i18n.t('screens.equipment.mascotDialog.button'),
icon: 'check', icon: 'check',
@ -207,4 +220,4 @@ class EquipmentListScreen extends React.Component<PropsType, StateType> {
} }
} }
export default withTheme(EquipmentListScreen); export default EquipmentListScreen;

View file

@ -17,8 +17,6 @@
* along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
*/ */
// @flow
import * as React from 'react'; import * as React from 'react';
import { import {
Button, Button,
@ -28,13 +26,12 @@ import {
Subheading, Subheading,
withTheme, withTheme,
} from 'react-native-paper'; } from 'react-native-paper';
import {StackNavigationProp} from '@react-navigation/stack'; import {StackNavigationProp, StackScreenProps} from '@react-navigation/stack';
import {BackHandler, View} from 'react-native'; import {BackHandler, View} from 'react-native';
import * as Animatable from 'react-native-animatable'; import * as Animatable from 'react-native-animatable';
import i18n from 'i18n-js'; import i18n from 'i18n-js';
import {CalendarList} from 'react-native-calendars'; import {CalendarList, PeriodMarking} from 'react-native-calendars';
import type {DeviceType} from './EquipmentListScreen'; import type {DeviceType} from './EquipmentListScreen';
import type {CustomThemeType} from '../../../managers/ThemeManager';
import LoadingConfirmDialog from '../../../components/Dialogs/LoadingConfirmDialog'; import LoadingConfirmDialog from '../../../components/Dialogs/LoadingConfirmDialog';
import ErrorDialog from '../../../components/Dialogs/ErrorDialog'; import ErrorDialog from '../../../components/Dialogs/ErrorDialog';
import { import {
@ -47,43 +44,46 @@ import {
} 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';
import {MainStackParamsList} from '../../../navigation/MainNavigator';
type PropsType = { type EquipmentRentScreenNavigationProp = StackScreenProps<
navigation: StackNavigationProp, MainStackParamsList,
route: { 'equipment-rent'
params?: { >;
item?: DeviceType,
}, type Props = EquipmentRentScreenNavigationProp & {
}, navigation: StackNavigationProp<any>;
theme: CustomThemeType, theme: ReactNativePaper.Theme;
}; };
export type MarkedDatesObjectType = { export type MarkedDatesObjectType = {
[key: string]: {startingDay: boolean, endingDay: boolean, color: string}, [key: string]: PeriodMarking;
}; };
type StateType = { type StateType = {
dialogVisible: boolean, dialogVisible: boolean;
errorDialogVisible: boolean, errorDialogVisible: boolean;
markedDates: MarkedDatesObjectType, markedDates: MarkedDatesObjectType;
currentError: number, currentError: number;
}; };
class EquipmentRentScreen extends React.Component<PropsType, StateType> { class EquipmentRentScreen extends React.Component<Props, StateType> {
item: DeviceType | null; item: DeviceType | null;
bookedDates: Array<string>; bookedDates: Array<string>;
bookRef: {current: null | Animatable.View}; bookRef: {current: null | (Animatable.View & View)};
canBookEquipment: boolean; canBookEquipment: boolean;
lockedDates: { lockedDates: {
[key: string]: {startingDay: boolean, endingDay: boolean, color: string}, [key: string]: PeriodMarking;
}; };
constructor(props: PropsType) { constructor(props: Props) {
super(props); super(props);
this.item = null;
this.lockedDates = {};
this.state = { this.state = {
dialogVisible: false, dialogVisible: false,
errorDialogVisible: false, errorDialogVisible: false,
@ -95,13 +95,16 @@ class EquipmentRentScreen extends React.Component<PropsType, StateType> {
this.canBookEquipment = false; this.canBookEquipment = false;
this.bookedDates = []; this.bookedDates = [];
if (props.route.params != null) { if (props.route.params != null) {
if (props.route.params.item != null) this.item = props.route.params.item; if (props.route.params.item != null) {
else this.item = null; this.item = props.route.params.item;
} else {
this.item = null;
}
} }
const {item} = this; const {item} = this;
if (item != null) { if (item != null) {
this.lockedDates = {}; this.lockedDates = {};
item.booked_at.forEach((date: {begin: string, end: string}) => { item.booked_at.forEach((date: {begin: string; end: string}) => {
const range = getValidRange( const range = getValidRange(
new Date(date.begin), new Date(date.begin),
new Date(date.end), new Date(date.end),
@ -211,11 +214,11 @@ class EquipmentRentScreen extends React.Component<PropsType, StateType> {
* @param day The day selected * @param day The day selected
*/ */
selectNewDate = (day: { selectNewDate = (day: {
dateString: string, dateString: string;
day: number, day: number;
month: number, month: number;
timestamp: number, timestamp: number;
year: number, year: number;
}) => { }) => {
const selected = new Date(day.dateString); const selected = new Date(day.dateString);
const start = this.getBookStartDate(); const start = this.getBookStartDate();
@ -229,7 +232,9 @@ class EquipmentRentScreen extends React.Component<PropsType, StateType> {
} 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 this.resetSelection(); } else {
this.resetSelection();
}
this.updateMarkedSelection(); this.updateMarkedSelection();
} }
}; };
@ -249,7 +254,7 @@ class EquipmentRentScreen extends React.Component<PropsType, StateType> {
* Shows the book button by plying a fade animation * Shows the book button by plying a fade animation
*/ */
showBookButton() { showBookButton() {
if (this.bookRef.current != null) { if (this.bookRef.current && this.bookRef.current.fadeInUp) {
this.bookRef.current.fadeInUp(500); this.bookRef.current.fadeInUp(500);
} }
} }
@ -258,7 +263,7 @@ class EquipmentRentScreen extends React.Component<PropsType, StateType> {
* Hides the book button by plying a fade animation * Hides the book button by plying a fade animation
*/ */
hideBookButton() { hideBookButton() {
if (this.bookRef.current != null) { if (this.bookRef.current && this.bookRef.current.fadeOutDown) {
this.bookRef.current.fadeOutDown(500); this.bookRef.current.fadeOutDown(500);
} }
} }
@ -271,7 +276,9 @@ class EquipmentRentScreen extends React.Component<PropsType, StateType> {
} }
resetSelection() { resetSelection() {
if (this.canBookEquipment) this.hideBookButton(); if (this.canBookEquipment) {
this.hideBookButton();
}
this.canBookEquipment = false; this.canBookEquipment = false;
this.bookedDates = []; this.bookedDates = [];
} }
@ -287,21 +294,23 @@ class EquipmentRentScreen extends React.Component<PropsType, StateType> {
}); });
} }
render(): React.Node { render() {
const {item, props, state} = this; const {item, props, state} = this;
const start = this.getBookStartDate(); const start = this.getBookStartDate();
const end = this.getBookEndDate(); const end = this.getBookEndDate();
let subHeadingText; let subHeadingText;
if (start == null) subHeadingText = i18n.t('screens.equipment.booking'); if (start == null) {
else if (end != null && start.getTime() !== end.getTime()) subHeadingText = i18n.t('screens.equipment.booking');
} else if (end != null && start.getTime() !== end.getTime()) {
subHeadingText = i18n.t('screens.equipment.bookingPeriod', { subHeadingText = i18n.t('screens.equipment.bookingPeriod', {
begin: getRelativeDateString(start), begin: getRelativeDateString(start),
end: getRelativeDateString(end), end: getRelativeDateString(end),
}); });
else } else {
subHeadingText = i18n.t('screens.equipment.bookingDay', { subHeadingText = i18n.t('screens.equipment.bookingDay', {
date: getRelativeDateString(start), date: getRelativeDateString(start),
}); });
}
if (item != null) { if (item != null) {
const isAvailable = isEquipmentAvailable(item); const isAvailable = isEquipmentAvailable(item);
const firstAvailability = getFirstEquipmentAvailability(item); const firstAvailability = getFirstEquipmentAvailability(item);
@ -369,12 +378,10 @@ class EquipmentRentScreen extends React.Component<PropsType, StateType> {
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
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, ...state.markedDates}} markedDates={{...this.lockedDates, ...state.markedDates}}
theme={{ theme={{
backgroundColor: props.theme.colors.agendaBackgroundColor, backgroundColor: props.theme.colors.agendaBackgroundColor,

View file

@ -21,6 +21,7 @@ import i18n from 'i18n-js';
import type {DeviceType} from '../screens/Amicale/Equipment/EquipmentListScreen'; import type {DeviceType} from '../screens/Amicale/Equipment/EquipmentListScreen';
import DateManager from '../managers/DateManager'; import DateManager from '../managers/DateManager';
import type {MarkedDatesObjectType} from '../screens/Amicale/Equipment/EquipmentRentScreen'; import type {MarkedDatesObjectType} from '../screens/Amicale/Equipment/EquipmentRentScreen';
import {PeriodMarking} from 'react-native-calendars';
/** /**
* Gets the current day at midnight * Gets the current day at midnight
@ -189,11 +190,7 @@ export function generateMarkedDates(
range: Array<string>, range: Array<string>,
): MarkedDatesObjectType { ): MarkedDatesObjectType {
const markedDates: { const markedDates: {
[key: string]: { [key: string]: PeriodMarking;
startingDay: boolean;
endingDay: boolean;
color: string;
};
} = {}; } = {};
for (let i = 0; i < range.length; i += 1) { for (let i = 0; i < range.length; i += 1) {
const isStart = i === 0; const isStart = i === 0;