Browse Source

Added basic equipment booking functionality

Arnaud Vergnet 1 year ago
parent
commit
5067fd47d6

+ 2
- 1
package.json View File

@@ -21,6 +21,7 @@
21 21
   "dependencies": {
22 22
     "@nartc/react-native-barcode-mask": "^1.2.0",
23 23
     "@react-native-community/async-storage": "^1.11.0",
24
+    "@react-native-community/datetimepicker": "^2.6.0",
24 25
     "@react-native-community/masked-view": "^0.1.10",
25 26
     "@react-native-community/push-notification-ios": "^1.2.2",
26 27
     "@react-native-community/slider": "^3.0.0",
@@ -34,7 +35,7 @@
34 35
     "react-native-app-intro-slider": "^4.0.0",
35 36
     "react-native-appearance": "^0.3.3",
36 37
     "react-native-autolink": "^3.0.0",
37
-    "react-native-calendars": "^1.299.0",
38
+    "react-native-calendars": "^1.300.0",
38 39
     "react-native-camera": "^3.30.0",
39 40
     "react-native-collapsible": "^1.5.2",
40 41
     "react-native-gesture-handler": "^1.6.1",

+ 86
- 0
src/components/Lists/Equipment/EquipmentListItem.js View File

@@ -0,0 +1,86 @@
1
+// @flow
2
+
3
+import * as React from 'react';
4
+import {Avatar, List, withTheme} from 'react-native-paper';
5
+import type {CustomTheme} from "../../../managers/ThemeManager";
6
+import type {Device} from "../../../screens/Amicale/Equipment/EquipmentListScreen";
7
+import i18n from "i18n-js";
8
+import {getTimeOnlyString, stringToDate} from "../../../utils/Planning";
9
+
10
+type Props = {
11
+    onPress: () => void,
12
+    item: Device,
13
+    height: number,
14
+    theme: CustomTheme,
15
+}
16
+
17
+class EquipmentListItem extends React.Component<Props> {
18
+
19
+    shouldComponentUpdate() {
20
+        return false;
21
+    }
22
+
23
+    isAvailable() {
24
+        const availableDate = stringToDate(this.props.item.available_at);
25
+        return availableDate != null && availableDate < new Date();
26
+    }
27
+
28
+    /**
29
+     * Gets the string representation of the given date.
30
+     *
31
+     * If the given date is the same day as today, only return the tile.
32
+     * Otherwise, return the full date.
33
+     *
34
+     * @param dateString The string representation of the wanted date
35
+     * @returns {string}
36
+     */
37
+    getDateString(dateString: string): string {
38
+        const today = new Date();
39
+        const date = stringToDate(dateString);
40
+        if (date != null && today.getDate() === date.getDate()) {
41
+            const str = getTimeOnlyString(dateString);
42
+            return str != null ? str : "";
43
+        } else
44
+            return dateString;
45
+    }
46
+
47
+
48
+    render() {
49
+        const colors = this.props.theme.colors;
50
+        const item = this.props.item;
51
+        const isAvailable = this.isAvailable();
52
+        return (
53
+            <List.Item
54
+                title={item.name}
55
+                description={isAvailable
56
+                    ? i18n.t('equipmentScreen.bail', {cost: item.caution})
57
+                    : i18n.t('equipmentScreen.availableAt', {date: this.getDateString(item.available_at)})}
58
+                onPress={this.props.onPress}
59
+                left={(props) => <Avatar.Icon
60
+                    {...props}
61
+                    style={{
62
+                        backgroundColor: 'transparent',
63
+                    }}
64
+                    icon={isAvailable ? "check-circle-outline" : "update"}
65
+                    color={isAvailable ? colors.success : colors.primary}
66
+                />}
67
+                right={(props) => <Avatar.Icon
68
+                    {...props}
69
+                    style={{
70
+                        marginTop: 'auto',
71
+                        marginBottom: 'auto',
72
+                        backgroundColor: 'transparent',
73
+                    }}
74
+                    size={48}
75
+                    icon={"chevron-right"}
76
+                />}
77
+                style={{
78
+                    height: this.props.height,
79
+                    justifyContent: 'center',
80
+                }}
81
+            />
82
+        );
83
+    }
84
+}
85
+
86
+export default withTheme(EquipmentListItem);

+ 4
- 0
src/navigation/MainNavigator.js View File

@@ -23,6 +23,8 @@ import ClubDisplayScreen from "../screens/Amicale/Clubs/ClubDisplayScreen";
23 23
 import {createScreenCollapsibleStack, getWebsiteStack} from "../utils/CollapsibleUtils";
24 24
 import BugReportScreen from "../screens/Other/FeedbackScreen";
25 25
 import WebsiteScreen from "../screens/Services/WebsiteScreen";
26
+import EquipmentScreen from "../screens/Amicale/Equipment/EquipmentListScreen";
27
+import EquipmentLendScreen from "../screens/Amicale/Equipment/EquipmentRentScreen";
26 28
 
27 29
 const modalTransition = Platform.OS === 'ios' ? TransitionPresets.ModalPresentationIOS : TransitionPresets.ModalSlideFromBottomIOS;
28 30
 
@@ -119,6 +121,8 @@ function MainStackComponent(props: { createTabNavigator: () => React.Node }) {
119 121
 
120 122
             {createScreenCollapsibleStack("profile", MainStack, ProfileScreen, i18n.t('screens.profile'))}
121 123
             {createScreenCollapsibleStack("club-list", MainStack, ClubListScreen, i18n.t('clubs.clubList'))}
124
+            {createScreenCollapsibleStack("equipment-list", MainStack, EquipmentScreen, i18n.t('screens.equipmentList'))}
125
+            {createScreenCollapsibleStack("equipment-lend", MainStack, EquipmentLendScreen, i18n.t('screens.equipmentLend'))}
122 126
             <MainStack.Screen
123 127
                 name="club-information"
124 128
                 component={ClubDisplayScreen}

+ 145
- 0
src/screens/Amicale/Equipment/EquipmentListScreen.js View File

@@ -0,0 +1,145 @@
1
+// @flow
2
+
3
+import * as React from 'react';
4
+import {Animated} from "react-native";
5
+import {Avatar, Card, Paragraph, withTheme} from 'react-native-paper';
6
+import AuthenticatedScreen from "../../../components/Amicale/AuthenticatedScreen";
7
+import {Collapsible} from "react-navigation-collapsible";
8
+import {withCollapsible} from "../../../utils/withCollapsible";
9
+import {StackNavigationProp} from "@react-navigation/stack";
10
+import type {CustomTheme} from "../../../managers/ThemeManager";
11
+import i18n from "i18n-js";
12
+import type {club} from "../Clubs/ClubListScreen";
13
+import EquipmentListItem from "../../../components/Lists/Equipment/EquipmentListItem";
14
+
15
+type Props = {
16
+    navigation: StackNavigationProp,
17
+    theme: CustomTheme,
18
+    collapsibleStack: Collapsible,
19
+}
20
+
21
+export type Device = {
22
+    id: number,
23
+    name: string,
24
+    caution: number,
25
+    available_at: string,
26
+};
27
+
28
+const TEST_DATASET = [
29
+    {
30
+        id: 1,
31
+        name: "Petit barbecue",
32
+        caution: 100,
33
+        available_at: "2020-07-07 21:12"
34
+    },
35
+    {
36
+        id: 2,
37
+        name: "Grand barbecue",
38
+        caution: 100,
39
+        available_at: "2020-07-08 21:12"
40
+    },
41
+    {
42
+        id: 3,
43
+        name: "Appareil à fondue",
44
+        caution: 100,
45
+        available_at: "2020-07-09 14:12"
46
+    },
47
+    {
48
+        id: 4,
49
+        name: "Appareil à croque-monsieur",
50
+        caution: 100,
51
+        available_at: "2020-07-10 12:12"
52
+    }
53
+]
54
+
55
+const ICON_AMICALE = require('../../../../assets/amicale.png');
56
+const LIST_ITEM_HEIGHT = 64;
57
+
58
+class EquipmentListScreen extends React.Component<Props> {
59
+
60
+    data: Array<Device>;
61
+
62
+    getRenderItem = ({item}: { item: Device }) => {
63
+        return (
64
+            <EquipmentListItem
65
+                onPress={() => this.props.navigation.navigate('equipment-lend', {item: item})}
66
+                item={item}
67
+                height={LIST_ITEM_HEIGHT}/>
68
+        );
69
+    };
70
+
71
+    /**
72
+     * Gets the list header, with explains this screen's purpose
73
+     *
74
+     * @returns {*}
75
+     */
76
+    getListHeader() {
77
+        return <Card style={{margin: 5}}>
78
+            <Card.Title
79
+                title={i18n.t('equipmentScreen.title')}
80
+                left={(props) => <Avatar.Image
81
+                    {...props}
82
+                    source={ICON_AMICALE}
83
+                    style={{backgroundColor: 'transparent'}}
84
+                />}
85
+            />
86
+            <Card.Content>
87
+                <Paragraph>
88
+                    {i18n.t('equipmentScreen.message')}
89
+                </Paragraph>
90
+            </Card.Content>
91
+        </Card>;
92
+    }
93
+
94
+    keyExtractor = (item: club) => item.id.toString();
95
+
96
+    /**
97
+     * Gets the main screen component with the fetched data
98
+     *
99
+     * @param data The data fetched from the server
100
+     * @returns {*}
101
+     */
102
+    getScreen = (data: Array<{ [key: string]: any } | null>) => {
103
+        if (data[0] != null) {
104
+            const fetchedData = data[0];
105
+            if (fetchedData != null)
106
+                this.data = fetchedData["devices"];
107
+
108
+            this.data = TEST_DATASET; // TODO remove in prod
109
+        }
110
+        const {containerPaddingTop, scrollIndicatorInsetTop, onScroll} = this.props.collapsibleStack;
111
+        return (
112
+            <Animated.FlatList
113
+                keyExtractor={this.keyExtractor}
114
+                renderItem={this.getRenderItem}
115
+                ListHeaderComponent={this.getListHeader()}
116
+                data={this.data}
117
+                // Animations
118
+                onScroll={onScroll}
119
+                contentContainerStyle={{
120
+                    paddingTop: containerPaddingTop,
121
+                    minHeight: '100%'
122
+                }}
123
+                scrollIndicatorInsets={{top: scrollIndicatorInsetTop}}
124
+            />
125
+        )
126
+    };
127
+
128
+    render() {
129
+        return (
130
+            <AuthenticatedScreen
131
+                {...this.props}
132
+                requests={[
133
+                    {
134
+                        link: 'user/profile',
135
+                        params: {},
136
+                        mandatory: false,
137
+                    }
138
+                ]}
139
+                renderFunction={this.getScreen}
140
+            />
141
+        );
142
+    }
143
+}
144
+
145
+export default withCollapsible(withTheme(EquipmentListScreen));

+ 569
- 0
src/screens/Amicale/Equipment/EquipmentRentScreen.js View File

@@ -0,0 +1,569 @@
1
+// @flow
2
+
3
+import * as React from 'react';
4
+import {Button, Caption, Card, Headline, Subheading, Text, withTheme} from 'react-native-paper';
5
+import {Collapsible} from "react-navigation-collapsible";
6
+import {withCollapsible} from "../../../utils/withCollapsible";
7
+import {StackNavigationProp} from "@react-navigation/stack";
8
+import type {CustomTheme} from "../../../managers/ThemeManager";
9
+import type {Device} from "./EquipmentListScreen";
10
+import {Animated, BackHandler} from "react-native";
11
+import * as Animatable from "react-native-animatable";
12
+import {View} from "react-native-animatable";
13
+import i18n from "i18n-js";
14
+import {dateToString, getTimeOnlyString, stringToDate} from "../../../utils/Planning";
15
+import {CalendarList} from "react-native-calendars";
16
+import DateTimePicker from '@react-native-community/datetimepicker';
17
+import LoadingConfirmDialog from "../../../components/Dialogs/LoadingConfirmDialog";
18
+import ConnectionManager from "../../../managers/ConnectionManager";
19
+import ErrorDialog from "../../../components/Dialogs/ErrorDialog";
20
+
21
+type Props = {
22
+    navigation: StackNavigationProp,
23
+    route: {
24
+        params?: {
25
+            item?: Device,
26
+        },
27
+    },
28
+    theme: CustomTheme,
29
+    collapsibleStack: Collapsible,
30
+}
31
+
32
+type State = {
33
+    dialogVisible: boolean,
34
+    errorDialogVisible: boolean,
35
+    markedDates: { [key: string]: { startingDay: boolean, endingDay: boolean, color: string } },
36
+    timePickerVisible: boolean,
37
+    currentError: number,
38
+}
39
+
40
+class EquipmentRentScreen extends React.Component<Props, State> {
41
+
42
+    state = {
43
+        dialogVisible: false,
44
+        errorDialogVisible: false,
45
+        markedDates: {},
46
+        timePickerVisible: false,
47
+        currentError: 0,
48
+    }
49
+
50
+    item: Device | null;
51
+    selectedDates: {
52
+        start: Date | null,
53
+        end: Date | null,
54
+    };
55
+
56
+    currentlySelectedDate: Date | null;
57
+
58
+    bookRef: { current: null | Animatable.View }
59
+    canBookEquipment: boolean;
60
+
61
+    constructor(props: Props) {
62
+        super(props);
63
+        this.resetSelection();
64
+        this.bookRef = React.createRef();
65
+        this.canBookEquipment = false;
66
+        if (this.props.route.params != null) {
67
+            if (this.props.route.params.item != null)
68
+                this.item = this.props.route.params.item;
69
+            else
70
+                this.item = null;
71
+        }
72
+    }
73
+
74
+    /**
75
+     * Captures focus and blur events to hook on android back button
76
+     */
77
+    componentDidMount() {
78
+        this.props.navigation.addListener(
79
+            'focus',
80
+            () =>
81
+                BackHandler.addEventListener(
82
+                    'hardwareBackPress',
83
+                    this.onBackButtonPressAndroid
84
+                )
85
+        );
86
+        this.props.navigation.addListener(
87
+            'blur',
88
+            () =>
89
+                BackHandler.removeEventListener(
90
+                    'hardwareBackPress',
91
+                    this.onBackButtonPressAndroid
92
+                )
93
+        );
94
+    }
95
+
96
+    /**
97
+     * Overrides default android back button behaviour to deselect date if any is selected.
98
+     *
99
+     * @return {boolean}
100
+     */
101
+    onBackButtonPressAndroid = () => {
102
+        if (this.currentlySelectedDate != null) {
103
+            this.resetSelection();
104
+            this.setState({
105
+                markedDates: this.generateMarkedDates(),
106
+            });
107
+            return true;
108
+        } else
109
+            return false;
110
+    };
111
+
112
+    isAvailable(item: Device) {
113
+        const availableDate = stringToDate(item.available_at);
114
+        return availableDate != null && availableDate < new Date();
115
+    }
116
+
117
+    /**
118
+     * Gets the string representation of the given date.
119
+     *
120
+     * If the given date is the same day as today, only return the tile.
121
+     * Otherwise, return the full date.
122
+     *
123
+     * @param dateString The string representation of the wanted date
124
+     * @returns {string}
125
+     */
126
+    getDateString(dateString: string): string {
127
+        const today = new Date();
128
+        const date = stringToDate(dateString);
129
+        if (date != null && today.getDate() === date.getDate()) {
130
+            const str = getTimeOnlyString(dateString);
131
+            return str != null ? str : "";
132
+        } else
133
+            return dateString;
134
+    }
135
+
136
+    /**
137
+     * Gets the minimum date for renting equipment
138
+     *
139
+     * @param item The item to rent
140
+     * @param isAvailable True is it is available right now
141
+     * @returns {Date}
142
+     */
143
+    getMinDate(item: Device, isAvailable: boolean) {
144
+        let date = new Date();
145
+        if (isAvailable)
146
+            return date;
147
+        else {
148
+            const limit = stringToDate(item.available_at)
149
+            return limit != null ? limit : date;
150
+        }
151
+    }
152
+
153
+    /**
154
+     * Selects a new date on the calendar.
155
+     * If both start and end dates are already selected, unselect all.
156
+     *
157
+     * @param day The day selected
158
+     */
159
+    selectNewDate = (day: { dateString: string, day: number, month: number, timestamp: number, year: number }) => {
160
+        this.currentlySelectedDate = new Date(day.dateString);
161
+
162
+        if (!this.canBookEquipment) {
163
+            const start = this.selectedDates.start;
164
+            if (start == null)
165
+                this.selectedDates.start = this.currentlySelectedDate;
166
+            else if (this.currentlySelectedDate < start) {
167
+                this.selectedDates.end = start;
168
+                this.selectedDates.start = this.currentlySelectedDate;
169
+            } else
170
+                this.selectedDates.end = this.currentlySelectedDate;
171
+        } else
172
+            this.resetSelection();
173
+
174
+        if (this.selectedDates.start != null) {
175
+            this.setState({
176
+                markedDates: this.generateMarkedDates(),
177
+                timePickerVisible: true,
178
+            });
179
+        } else {
180
+            this.setState({
181
+                markedDates: this.generateMarkedDates(),
182
+            });
183
+        }
184
+    }
185
+
186
+    resetSelection() {
187
+        if (this.canBookEquipment)
188
+            this.hideBookButton();
189
+        this.canBookEquipment = false;
190
+        this.selectedDates = {start: null, end: null};
191
+        this.currentlySelectedDate = null;
192
+    }
193
+
194
+    /**
195
+     * Deselect the currently selected date
196
+     */
197
+    deselectCurrentDate() {
198
+        let currentlySelectedDate = this.currentlySelectedDate;
199
+        const start = this.selectedDates.start;
200
+        const end = this.selectedDates.end;
201
+        if (currentlySelectedDate != null && start != null) {
202
+            if (currentlySelectedDate === start && end === null)
203
+                this.resetSelection();
204
+            else if (end != null && currentlySelectedDate === end) {
205
+                this.currentlySelectedDate = start;
206
+                this.selectedDates.end = null;
207
+            } else if (currentlySelectedDate === start) {
208
+                this.currentlySelectedDate = end;
209
+                this.selectedDates.start = this.selectedDates.end;
210
+                this.selectedDates.end = null;
211
+            }
212
+        }
213
+    }
214
+
215
+    /**
216
+     * Saves the selected time to the currently selected date.
217
+     * If no the time selection was canceled, cancels the current selecction
218
+     *
219
+     * @param event The click event
220
+     * @param date The date selected
221
+     */
222
+    onTimeChange = (event: { nativeEvent: { timestamp: number }, type: string }, date: Date) => {
223
+        let currentDate = this.currentlySelectedDate;
224
+        const item = this.item;
225
+        if (item != null && event.type === "set" && currentDate != null) {
226
+            currentDate.setHours(date.getHours());
227
+            currentDate.setMinutes(date.getMinutes());
228
+
229
+            const isAvailable = this.isAvailable(item);
230
+            let limit = this.getMinDate(item, isAvailable);
231
+            // Prevent selecting a date before now
232
+            if (this.getISODate(currentDate) === this.getISODate(limit) && currentDate < limit) {
233
+                currentDate.setHours(limit.getHours());
234
+                currentDate.setMinutes(limit.getMinutes());
235
+            }
236
+
237
+            if (this.selectedDates.start != null && this.selectedDates.end != null) {
238
+                if (this.selectedDates.start > this.selectedDates.end) {
239
+                    const temp = this.selectedDates.start;
240
+                    this.selectedDates.start = this.selectedDates.end;
241
+                    this.selectedDates.end = temp;
242
+                }
243
+                this.canBookEquipment = true;
244
+                this.showBookButton();
245
+            }
246
+        } else
247
+            this.deselectCurrentDate();
248
+
249
+        this.setState({
250
+            timePickerVisible: false,
251
+            markedDates: this.generateMarkedDates(),
252
+        });
253
+    }
254
+
255
+    /**
256
+     * Returns the ISO date format (without the time)
257
+     *
258
+     * @param date The date to recover the ISO format from
259
+     * @returns {*}
260
+     */
261
+    getISODate(date: Date) {
262
+        return date.toISOString().split("T")[0];
263
+    }
264
+
265
+    /**
266
+     * Generates the object containing all marked dates between the start and end dates selected
267
+     *
268
+     * @returns {{}}
269
+     */
270
+    generateMarkedDates() {
271
+        let markedDates = {}
272
+        const start = this.selectedDates.start;
273
+        const end = this.selectedDates.end;
274
+        if (start != null) {
275
+            const startISODate = this.getISODate(start);
276
+            if (end != null && this.getISODate(end) !== startISODate) {
277
+                markedDates[startISODate] = {
278
+                    startingDay: true,
279
+                    endingDay: false,
280
+                    color: this.props.theme.colors.primary
281
+                };
282
+                markedDates[this.getISODate(end)] = {
283
+                    startingDay: false,
284
+                    endingDay: true,
285
+                    color: this.props.theme.colors.primary
286
+                };
287
+                let date = new Date(start);
288
+                date.setDate(date.getDate() + 1);
289
+                while (date < end && this.getISODate(date) !== this.getISODate(end)) {
290
+                    markedDates[this.getISODate(date)] =
291
+                        {startingDay: false, endingDay: false, color: this.props.theme.colors.danger};
292
+                    date.setDate(date.getDate() + 1);
293
+                }
294
+            } else {
295
+                markedDates[startISODate] = {
296
+                    startingDay: true,
297
+                    endingDay: true,
298
+                    color: this.props.theme.colors.primary
299
+                };
300
+            }
301
+        }
302
+        return markedDates;
303
+    }
304
+
305
+    /**
306
+     * Shows the book button by plying a fade animation
307
+     */
308
+    showBookButton() {
309
+        if (this.bookRef.current != null) {
310
+            this.bookRef.current.fadeInUp(500);
311
+        }
312
+    }
313
+
314
+    /**
315
+     * Hides the book button by plying a fade animation
316
+     */
317
+    hideBookButton() {
318
+        if (this.bookRef.current != null) {
319
+            this.bookRef.current.fadeOutDown(500);
320
+        }
321
+    }
322
+
323
+    showDialog = () => {
324
+        this.setState({dialogVisible: true});
325
+    }
326
+
327
+    showErrorDialog = (error: number) => {
328
+        this.setState({
329
+            errorDialogVisible: true,
330
+            currentError: error,
331
+        });
332
+    }
333
+
334
+    onDialogDismiss = () => {
335
+        this.setState({dialogVisible: false});
336
+    }
337
+
338
+    onErrorDialogDismiss = () => {
339
+        this.setState({errorDialogVisible: false});
340
+    }
341
+
342
+    /**
343
+     * Sends the selected data to the server and waits for a response.
344
+     * If the request is a success, navigate to the recap screen.
345
+     * If it is an error, display the error to the user.
346
+     *
347
+     * @returns {Promise<R>}
348
+     */
349
+    onDialogAccept = () => {
350
+        return new Promise((resolve) => {
351
+            const item = this.item;
352
+            const start = this.selectedDates.start;
353
+            const end = this.selectedDates.end;
354
+            if (item != null && start != null && end != null) {
355
+                ConnectionManager.getInstance().authenticatedRequest(
356
+                    "", // TODO set path
357
+                    {
358
+                        "id": item.id,
359
+                        "start": dateToString(start, false),
360
+                        "end": dateToString(end, false),
361
+                    })
362
+                    .then(() => {
363
+                        console.log("Success, replace screen");
364
+                        resolve();
365
+                    })
366
+                    .catch((error: number) => {
367
+                        this.onDialogDismiss();
368
+                        this.showErrorDialog(error);
369
+                        resolve();
370
+                    });
371
+            } else
372
+                resolve();
373
+        });
374
+    }
375
+
376
+    render() {
377
+        const {containerPaddingTop, scrollIndicatorInsetTop, onScroll} = this.props.collapsibleStack;
378
+        let startString = <Caption>{i18n.t('equipmentScreen.notSet')}</Caption>;
379
+        let endString = <Caption>{i18n.t('equipmentScreen.notSet')}</Caption>;
380
+        const start = this.selectedDates.start;
381
+        const end = this.selectedDates.end;
382
+        if (start != null)
383
+            startString = dateToString(start, false);
384
+        if (end != null)
385
+            endString = dateToString(end, false);
386
+
387
+        const item = this.item;
388
+        if (item != null) {
389
+            const isAvailable = this.isAvailable(item);
390
+            return (
391
+                <View style={{flex: 1}}>
392
+                    <Animated.ScrollView
393
+                        // Animations
394
+                        onScroll={onScroll}
395
+                        contentContainerStyle={{
396
+                            paddingTop: containerPaddingTop,
397
+                            minHeight: '100%'
398
+                        }}
399
+                        scrollIndicatorInsets={{top: scrollIndicatorInsetTop}}>
400
+                        <Card style={{margin: 5}}>
401
+                            <Card.Content>
402
+                                <View style={{flex: 1}}>
403
+                                    <View style={{
404
+                                        marginLeft: "auto",
405
+                                        marginRight: "auto",
406
+                                        flexDirection: "row",
407
+                                        flexWrap: "wrap",
408
+                                    }}>
409
+                                        <Headline style={{textAlign: "center"}}>
410
+                                            {item.name}
411
+                                        </Headline>
412
+                                        <Caption style={{
413
+                                            textAlign: "center",
414
+                                            lineHeight: 35,
415
+                                            marginLeft: 10,
416
+                                        }}>
417
+                                            ({i18n.t('equipmentScreen.bail', {cost: item.caution})})
418
+                                        </Caption>
419
+                                    </View>
420
+                                </View>
421
+
422
+                                <Button
423
+                                    icon={isAvailable ? "check-circle-outline" : "update"}
424
+                                    color={isAvailable ? this.props.theme.colors.success : this.props.theme.colors.primary}
425
+                                    mode="text"
426
+                                >
427
+                                    {
428
+                                        isAvailable
429
+                                            ? i18n.t('equipmentScreen.available')
430
+                                            : i18n.t('equipmentScreen.availableAt', {date: this.getDateString(item.available_at)})
431
+                                    }
432
+                                </Button>
433
+                                <Text style={{
434
+                                    textAlign: "center",
435
+                                    marginBottom: 10
436
+                                }}>
437
+                                    {i18n.t('equipmentScreen.booking')}
438
+                                </Text>
439
+                                <Subheading style={{textAlign: "center"}}>
440
+                                    {i18n.t('equipmentScreen.startDate')}
441
+                                    {startString}
442
+                                </Subheading>
443
+                                <Subheading style={{textAlign: "center"}}>
444
+                                    {i18n.t('equipmentScreen.endDate')}
445
+                                    {endString}
446
+                                </Subheading>
447
+                            </Card.Content>
448
+                        </Card>
449
+                        {this.state.timePickerVisible
450
+                            ? <DateTimePicker
451
+                                value={new Date()}
452
+                                mode={"time"}
453
+                                display={"clock"}
454
+                                is24Hour={true}
455
+                                onChange={this.onTimeChange}
456
+                            />
457
+                            : null}
458
+                        <CalendarList
459
+                            // Minimum date that can be selected, dates before minDate will be grayed out. Default = undefined
460
+                            minDate={this.getMinDate(item, isAvailable)}
461
+                            // Max amount of months allowed to scroll to the past. Default = 50
462
+                            pastScrollRange={0}
463
+                            // Max amount of months allowed to scroll to the future. Default = 50
464
+                            futureScrollRange={3}
465
+                            // Enable horizontal scrolling, default = false
466
+                            horizontal={true}
467
+                            // Enable paging on horizontal, default = false
468
+                            pagingEnabled={true}
469
+                            // Handler which gets executed on day press. Default = undefined
470
+                            onDayPress={this.selectNewDate}
471
+                            // If firstDay=1 week starts from Monday. Note that dayNames and dayNamesShort should still start from Sunday.
472
+                            firstDay={1}
473
+                            // Disable all touch events for disabled days. can be override with disableTouchEvent in markedDates
474
+                            disableAllTouchEventsForDisabledDays={true}
475
+                            // Hide month navigation arrows.
476
+                            hideArrows={false}
477
+                            // Date marking style [simple/period/multi-dot/custom]. Default = 'simple'
478
+                            markingType={'period'}
479
+                            markedDates={this.state.markedDates}
480
+
481
+                            theme={{
482
+                                backgroundColor: this.props.theme.colors.agendaBackgroundColor,
483
+                                calendarBackground: this.props.theme.colors.background,
484
+                                textSectionTitleColor: this.props.theme.colors.agendaDayTextColor,
485
+                                selectedDayBackgroundColor: this.props.theme.colors.primary,
486
+                                selectedDayTextColor: '#ffffff',
487
+                                todayTextColor: this.props.theme.colors.text,
488
+                                dayTextColor: this.props.theme.colors.text,
489
+                                textDisabledColor: this.props.theme.colors.agendaDayTextColor,
490
+                                dotColor: this.props.theme.colors.primary,
491
+                                selectedDotColor: '#ffffff',
492
+                                arrowColor: this.props.theme.colors.primary,
493
+                                monthTextColor: this.props.theme.colors.text,
494
+                                indicatorColor: this.props.theme.colors.primary,
495
+                                textDayFontFamily: 'monospace',
496
+                                textMonthFontFamily: 'monospace',
497
+                                textDayHeaderFontFamily: 'monospace',
498
+                                textDayFontWeight: '300',
499
+                                textMonthFontWeight: 'bold',
500
+                                textDayHeaderFontWeight: '300',
501
+                                textDayFontSize: 16,
502
+                                textMonthFontSize: 16,
503
+                                textDayHeaderFontSize: 16,
504
+                                'stylesheet.day.period': {
505
+                                    base: {
506
+                                        overflow: 'hidden',
507
+                                        height: 34,
508
+                                        width: 34,
509
+                                        alignItems: 'center',
510
+
511
+                                    }
512
+                                }
513
+                            }}
514
+                            style={{marginBottom: 50}}
515
+                        />
516
+                    </Animated.ScrollView>
517
+                    <LoadingConfirmDialog
518
+                        visible={this.state.dialogVisible}
519
+                        onDismiss={this.onDialogDismiss}
520
+                        onAccept={this.onDialogAccept}
521
+                        title={i18n.t('equipmentScreen.dialogTitle')}
522
+                        titleLoading={i18n.t('equipmentScreen.dialogTitleLoading')}
523
+                        message={i18n.t('equipmentScreen.dialogMessage')}
524
+                    />
525
+
526
+                    <ErrorDialog
527
+                        visible={this.state.errorDialogVisible}
528
+                        onDismiss={this.onErrorDialogDismiss}
529
+                        errorCode={this.state.currentError}
530
+                    />
531
+                    <Animatable.View
532
+                        ref={this.bookRef}
533
+                        style={{
534
+                            position: "absolute",
535
+                            bottom: 0,
536
+                            left: 0,
537
+                            width: "100%",
538
+                            flex: 1,
539
+                            transform: [
540
+                                {translateY: 100},
541
+                            ]
542
+                        }}>
543
+                        <Button
544
+                            icon="bookmark-check"
545
+                            mode="contained"
546
+                            onPress={this.showDialog}
547
+                            style={{
548
+                                width: "80%",
549
+                                flex: 1,
550
+                                marginLeft: "auto",
551
+                                marginRight: "auto",
552
+                                marginBottom: 20,
553
+                                borderRadius: 10
554
+                            }}
555
+                        >
556
+                            {i18n.t('equipmentScreen.bookButton')}
557
+                        </Button>
558
+                    </Animatable.View>
559
+
560
+                </View>
561
+
562
+            )
563
+        } else
564
+            return <View/>;
565
+    }
566
+
567
+}
568
+
569
+export default withCollapsible(withTheme(EquipmentRentScreen));

+ 7
- 0
src/screens/Services/ServicesScreen.js View File

@@ -33,6 +33,7 @@ const AMICALE_LOGO = require("../../../assets/amicale.png");
33 33
 
34 34
 const CLUBS_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Clubs.png";
35 35
 const PROFILE_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/ProfilAmicaliste.png";
36
+const EQUIPMENT_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Materiel.png";
36 37
 const VOTE_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/Vote.png";
37 38
 const AMICALE_IMAGE = "https://etud.insa-toulouse.fr/~amicale_app/images/WebsiteAmicale.png";
38 39
 
@@ -73,6 +74,12 @@ class ServicesScreen extends React.Component<Props> {
73 74
                 onPress: () => this.onAmicaleServicePress("profile"),
74 75
             },
75 76
             {
77
+                title: i18n.t('screens.equipmentList'),
78
+                subtitle: i18n.t('servicesScreen.descriptions.equipment'),
79
+                image: EQUIPMENT_IMAGE,
80
+                onPress: () => this.onAmicaleServicePress("equipment-list"),
81
+            },
82
+            {
76 83
                 title: i18n.t('screens.amicaleWebsite'),
77 84
                 subtitle: i18n.t('servicesScreen.descriptions.amicaleWebsite'),
78 85
                 image: AMICALE_IMAGE,

+ 20
- 2
translations/en.json View File

@@ -26,7 +26,9 @@
26 26
     "vote": "Elections",
27 27
     "scanner": "Scanotron 3000",
28 28
     "feedback": "Feedback",
29
-    "insaAccount": "INSA Account"
29
+    "insaAccount": "INSA Account",
30
+    "equipmentList": "Equipment Booking",
31
+    "equipmentLend": "Book"
30 32
   },
31 33
   "intro": {
32 34
     "slideMain": {
@@ -428,7 +430,8 @@
428 430
       "bib": "Book a Bib'Box for project work",
429 431
       "mails": "Check your INSA mails",
430 432
       "ent": "See your grades",
431
-      "insaAccount": "See your information and change your password"
433
+      "insaAccount": "See your information and change your password",
434
+      "equipment": "Book a BBQ or other equipment"
432 435
     }
433 436
   },
434 437
   "planningScreen": {
@@ -444,5 +447,20 @@
444 447
     "contactMeans": "Using Gitea is recommended, to use it simply login with your INSA account.",
445 448
     "homeButtonTitle": "Feedback/Bug report",
446 449
     "homeButtonSubtitle": "Contact the devs"
450
+  },
451
+  "equipmentScreen": {
452
+    "title": "Equipment booking",
453
+    "message": "Thanks to the Amicale, students have access to some equipment like BBQs and others. To book one of those items, click the equipment of your choice in the list bellow, enter your lend dates, then come around the Amicale to claim it and give your bail.",
454
+    "bail": "Bail: %{cost}€",
455
+    "availableAt": "Available at: %{date}",
456
+    "available": "Available!",
457
+    "booking": "Click on the calendar to set the start and end dates",
458
+    "startDate": "Start: ",
459
+    "endDate": "End: ",
460
+    "notSet": "Not set",
461
+    "bookButton": "Book selected dates",
462
+    "dialogTitle": "Confirm booking?",
463
+    "dialogTitleLoading": "Sending your booking...",
464
+    "dialogMessage": "Are you sure you want to confirm your booking?\n\nYou will then be able to claim the selected equipment at the Amicale for the duration of your booking in exchange of a bail."
447 465
   }
448 466
 }

+ 20
- 2
translations/fr.json View File

@@ -26,7 +26,9 @@
26 26
     "vote": "Élections",
27 27
     "scanner": "Scanotron 3000",
28 28
     "feedback": "Votre avis",
29
-    "insaAccount": "Compte INSA"
29
+    "insaAccount": "Compte INSA",
30
+    "equipmentList": "Réservation Matériel",
31
+    "equipmentLend": "Réserver"
30 32
   },
31 33
   "intro": {
32 34
     "slideMain": {
@@ -430,7 +432,8 @@
430 432
       "bib": "Réservez une Bib'Box pour les travaux de groupe",
431 433
       "mails": "Vérifiez vos mails INSA",
432 434
       "ent": "Retrouvez vos notes",
433
-      "insaAccount": "Accédez à vos informations et modifiez votre mot de passe"
435
+      "insaAccount": "Accédez à vos informations et modifiez votre mot de passe",
436
+      "equipment": "Réservez un BBQ ou d'autre matériel"
434 437
     }
435 438
   },
436 439
   "planningScreen": {
@@ -446,5 +449,20 @@
446 449
     "contactMeans": "L'utilisation de Gitea est recommandée, pour l'utiliser, connectez vous avec vos identifiants INSA.",
447 450
     "homeButtonTitle": "Feedback/Bugs",
448 451
     "homeButtonSubtitle": "Contacter le développeur"
452
+  },
453
+  "equipmentScreen": {
454
+    "title": "Réservation de Matériel",
455
+    "message": "L'Amicale mets à disposition des étudiants du matériel comme des BBQ, des appareils à raclette et autres. Pour réserver l'un de ces formidables appareils, cliquez sur celui de votre choix dans la liste, indiquez les dates du prêt, puis passez à l'Amicale pour le récupérer et donner votre caution.",
456
+    "bail": "Caution : %{cost}€",
457
+    "availableAt": "Disponible à : %{date}",
458
+    "available": "Disponible !",
459
+    "booking": "Cliquez sur le calendrier pour choisir les dates de début et de fin de la réservation",
460
+    "startDate": "Début: ",
461
+    "endDate": "Fin: ",
462
+    "notSet": "Non défini",
463
+    "bookButton": "Choisir ces dates",
464
+    "dialogTitle": "Confirmer la réservation ?",
465
+    "dialogTitleLoading": "Envoi de la réservation...",
466
+    "dialogMessage": "Êtes vous sûr de vouloir confirmer cette réservation?\n\nVous pourrez ensuite récupérer le matériel à l'Amicale pour la durée de votre reservation en échange d'une caution."
449 467
   }
450 468
 }

Loading…
Cancel
Save