Browse Source

Improved equipment rent screens to match new api version

Arnaud Vergnet 3 years ago
parent
commit
e048035722

+ 319
- 0
__tests__/utils/EquipmentBooking.test.js View File

@@ -0,0 +1,319 @@
1
+import React from 'react';
2
+import * as EquipmentBooking from "../../src/utils/EquipmentBooking";
3
+import i18n from "i18n-js";
4
+
5
+test('getISODate', () => {
6
+    let date = new Date("2020-03-05 12:00");
7
+    expect(EquipmentBooking.getISODate(date)).toBe("2020-03-05");
8
+    date = new Date("2020-03-05");
9
+    expect(EquipmentBooking.getISODate(date)).toBe("2020-03-05");
10
+    date = new Date("2020-03-05 00:00"); // Treated as local time
11
+    expect(EquipmentBooking.getISODate(date)).toBe("2020-03-04"); // Treated as UTC
12
+});
13
+
14
+test('getCurrentDay', () => {
15
+    jest.spyOn(Date, 'now')
16
+        .mockImplementation(() =>
17
+            new Date('2020-01-14 14:50:35').getTime()
18
+        );
19
+    expect(EquipmentBooking.getCurrentDay().getTime()).toBe(new Date("2020-01-14").getTime());
20
+});
21
+
22
+test('isEquipmentAvailable', () => {
23
+    jest.spyOn(Date, 'now')
24
+        .mockImplementation(() =>
25
+            new Date('2020-07-09').getTime()
26
+        );
27
+    let testDevice = {
28
+        id: 1,
29
+        name: "Petit barbecue",
30
+        caution: 100,
31
+        booked_at: [{begin: "2020-07-07", end: "2020-07-10"}]
32
+    };
33
+    expect(EquipmentBooking.isEquipmentAvailable(testDevice)).toBeFalse();
34
+
35
+    testDevice.booked_at = [{begin: "2020-07-07", end: "2020-07-09"}];
36
+    expect(EquipmentBooking.isEquipmentAvailable(testDevice)).toBeFalse();
37
+
38
+    testDevice.booked_at = [{begin: "2020-07-09", end: "2020-07-10"}];
39
+    expect(EquipmentBooking.isEquipmentAvailable(testDevice)).toBeFalse();
40
+
41
+    testDevice.booked_at = [
42
+        {begin: "2020-07-07", end: "2020-07-8"},
43
+        {begin: "2020-07-10", end: "2020-07-12"},
44
+    ];
45
+    expect(EquipmentBooking.isEquipmentAvailable(testDevice)).toBeTrue();
46
+});
47
+
48
+test('getFirstEquipmentAvailability', () => {
49
+    jest.spyOn(Date, 'now')
50
+        .mockImplementation(() =>
51
+            new Date('2020-07-09').getTime()
52
+        );
53
+    let testDevice = {
54
+        id: 1,
55
+        name: "Petit barbecue",
56
+        caution: 100,
57
+        booked_at: [{begin: "2020-07-07", end: "2020-07-10"}]
58
+    };
59
+    expect(EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime()).toBe(new Date("2020-07-11").getTime());
60
+    testDevice.booked_at = [{begin: "2020-07-07", end: "2020-07-09"}];
61
+    expect(EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime()).toBe(new Date("2020-07-10").getTime());
62
+    testDevice.booked_at = [
63
+        {begin: "2020-07-07", end: "2020-07-09"},
64
+        {begin: "2020-07-10", end: "2020-07-16"},
65
+    ];
66
+    expect(EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime()).toBe(new Date("2020-07-17").getTime());
67
+    testDevice.booked_at = [
68
+        {begin: "2020-07-07", end: "2020-07-09"},
69
+        {begin: "2020-07-10", end: "2020-07-12"},
70
+        {begin: "2020-07-14", end: "2020-07-16"},
71
+    ];
72
+    expect(EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime()).toBe(new Date("2020-07-13").getTime());
73
+});
74
+
75
+test('getRelativeDateString', () => {
76
+    jest.spyOn(Date, 'now')
77
+        .mockImplementation(() =>
78
+            new Date('2020-07-09').getTime()
79
+        );
80
+    jest.spyOn(i18n, 't')
81
+        .mockImplementation((translationString: string) => {
82
+                const prefix = "equipmentScreen.";
83
+                if (translationString === prefix + "otherYear")
84
+                    return "0";
85
+                else if (translationString === prefix + "otherMonth")
86
+                    return "1";
87
+                else if (translationString === prefix + "thisMonth")
88
+                    return "2";
89
+                else if (translationString === prefix + "tomorrow")
90
+                    return "3";
91
+                else if (translationString === prefix + "today")
92
+                    return "4";
93
+                else
94
+                    return null;
95
+            }
96
+        );
97
+    expect(EquipmentBooking.getRelativeDateString(new Date("2020-07-09"))).toBe("4");
98
+    expect(EquipmentBooking.getRelativeDateString(new Date("2020-07-10"))).toBe("3");
99
+    expect(EquipmentBooking.getRelativeDateString(new Date("2020-07-11"))).toBe("2");
100
+    expect(EquipmentBooking.getRelativeDateString(new Date("2020-07-30"))).toBe("2");
101
+    expect(EquipmentBooking.getRelativeDateString(new Date("2020-08-30"))).toBe("1");
102
+    expect(EquipmentBooking.getRelativeDateString(new Date("2020-11-10"))).toBe("1");
103
+    expect(EquipmentBooking.getRelativeDateString(new Date("2021-11-10"))).toBe("0");
104
+});
105
+
106
+test('getValidRange', () => {
107
+    let testDevice = {
108
+        id: 1,
109
+        name: "Petit barbecue",
110
+        caution: 100,
111
+        booked_at: [{begin: "2020-07-07", end: "2020-07-10"}]
112
+    };
113
+    let start = new Date("2020-07-11");
114
+    let end = new Date("2020-07-15");
115
+    let result = [
116
+        "2020-07-11",
117
+        "2020-07-12",
118
+        "2020-07-13",
119
+        "2020-07-14",
120
+        "2020-07-15",
121
+    ];
122
+    expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual(result);
123
+    testDevice.booked_at = [
124
+        {begin: "2020-07-07", end: "2020-07-10"},
125
+        {begin: "2020-07-13", end: "2020-07-15"},
126
+    ];
127
+    result = [
128
+        "2020-07-11",
129
+        "2020-07-12",
130
+    ];
131
+    expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual(result);
132
+
133
+    testDevice.booked_at = [{begin: "2020-07-12", end: "2020-07-13"}];
134
+    result = ["2020-07-11"];
135
+    expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual(result);
136
+    testDevice.booked_at = [{begin: "2020-07-07", end: "2020-07-12"},];
137
+    result = [
138
+        "2020-07-13",
139
+        "2020-07-14",
140
+        "2020-07-15",
141
+    ];
142
+    expect(EquipmentBooking.getValidRange(end, start, testDevice)).toStrictEqual(result);
143
+    start = new Date("2020-07-14");
144
+    end = new Date("2020-07-14");
145
+    result = [
146
+        "2020-07-14",
147
+    ];
148
+    expect(EquipmentBooking.getValidRange(start, start, testDevice)).toStrictEqual(result);
149
+    expect(EquipmentBooking.getValidRange(end, start, testDevice)).toStrictEqual(result);
150
+    expect(EquipmentBooking.getValidRange(start, end, null)).toStrictEqual(result);
151
+
152
+    start = new Date("2020-07-14");
153
+    end = new Date("2020-07-17");
154
+    result = [
155
+        "2020-07-14",
156
+        "2020-07-15",
157
+        "2020-07-16",
158
+        "2020-07-17",
159
+    ];
160
+    expect(EquipmentBooking.getValidRange(start, end, null)).toStrictEqual(result);
161
+
162
+    testDevice.booked_at = [{begin: "2020-07-17", end: "2020-07-17"}];
163
+    result = [
164
+        "2020-07-14",
165
+        "2020-07-15",
166
+        "2020-07-16",
167
+    ];
168
+    expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual(result);
169
+
170
+    testDevice.booked_at = [
171
+        {begin: "2020-07-12", end: "2020-07-13"},
172
+        {begin: "2020-07-15", end: "2020-07-20"},
173
+    ];
174
+    start = new Date("2020-07-11");
175
+    end = new Date("2020-07-23");
176
+    result = [
177
+        "2020-07-21",
178
+        "2020-07-22",
179
+        "2020-07-23",
180
+    ];
181
+    expect(EquipmentBooking.getValidRange(end, start, testDevice)).toStrictEqual(result);
182
+});
183
+
184
+test('generateMarkedDates', () => {
185
+    let theme = {
186
+        colors: {
187
+            primary: "primary",
188
+            danger: "primary",
189
+            textDisabled: "primary",
190
+        }
191
+    }
192
+    let testDevice = {
193
+        id: 1,
194
+        name: "Petit barbecue",
195
+        caution: 100,
196
+        booked_at: [{begin: "2020-07-07", end: "2020-07-10"}]
197
+    };
198
+    let start = new Date("2020-07-11");
199
+    let end = new Date("2020-07-13");
200
+    let range = EquipmentBooking.getValidRange(start, end, testDevice);
201
+    let result = {
202
+        "2020-07-11": {
203
+            startingDay: true,
204
+            endingDay: false,
205
+            color: theme.colors.primary
206
+        },
207
+        "2020-07-12": {
208
+            startingDay: false,
209
+            endingDay: false,
210
+            color: theme.colors.danger
211
+        },
212
+        "2020-07-13": {
213
+            startingDay: false,
214
+            endingDay: true,
215
+            color: theme.colors.primary
216
+        },
217
+    };
218
+    expect(EquipmentBooking.generateMarkedDates(true, theme, range)).toStrictEqual(result);
219
+    result = {
220
+        "2020-07-11": {
221
+            startingDay: true,
222
+            endingDay: false,
223
+            color: theme.colors.textDisabled
224
+        },
225
+        "2020-07-12": {
226
+            startingDay: false,
227
+            endingDay: false,
228
+            color: theme.colors.textDisabled
229
+        },
230
+        "2020-07-13": {
231
+            startingDay: false,
232
+            endingDay: true,
233
+            color: theme.colors.textDisabled
234
+        },
235
+    };
236
+    expect(EquipmentBooking.generateMarkedDates(false, theme, range)).toStrictEqual(result);
237
+    result = {
238
+        "2020-07-11": {
239
+            startingDay: true,
240
+            endingDay: false,
241
+            color: theme.colors.textDisabled
242
+        },
243
+        "2020-07-12": {
244
+            startingDay: false,
245
+            endingDay: false,
246
+            color: theme.colors.textDisabled
247
+        },
248
+        "2020-07-13": {
249
+            startingDay: false,
250
+            endingDay: true,
251
+            color: theme.colors.textDisabled
252
+        },
253
+    };
254
+    range = EquipmentBooking.getValidRange(end, start, testDevice);
255
+    expect(EquipmentBooking.generateMarkedDates(false, theme, range)).toStrictEqual(result);
256
+
257
+    testDevice.booked_at = [{begin: "2020-07-13", end: "2020-07-15"},];
258
+    result = {
259
+        "2020-07-11": {
260
+            startingDay: true,
261
+            endingDay: false,
262
+            color: theme.colors.primary
263
+        },
264
+        "2020-07-12": {
265
+            startingDay: false,
266
+            endingDay: true,
267
+            color: theme.colors.primary
268
+        },
269
+    };
270
+    range = EquipmentBooking.getValidRange(start, end, testDevice);
271
+    expect(EquipmentBooking.generateMarkedDates(true, theme, range)).toStrictEqual(result);
272
+
273
+    testDevice.booked_at = [{begin: "2020-07-12", end: "2020-07-13"},];
274
+    result = {
275
+        "2020-07-11": {
276
+            startingDay: true,
277
+            endingDay: true,
278
+            color: theme.colors.primary
279
+        },
280
+    };
281
+    range = EquipmentBooking.getValidRange(start, end, testDevice);
282
+    expect(EquipmentBooking.generateMarkedDates(true, theme, range)).toStrictEqual(result);
283
+
284
+    testDevice.booked_at = [
285
+        {begin: "2020-07-12", end: "2020-07-13"},
286
+        {begin: "2020-07-15", end: "2020-07-20"},
287
+    ];
288
+    start = new Date("2020-07-11");
289
+    end = new Date("2020-07-23");
290
+    result = {
291
+        "2020-07-11": {
292
+            startingDay: true,
293
+            endingDay: true,
294
+            color: theme.colors.primary
295
+        },
296
+    };
297
+    range = EquipmentBooking.getValidRange(start, end, testDevice);
298
+    expect(EquipmentBooking.generateMarkedDates(true, theme, range)).toStrictEqual(result);
299
+
300
+    result = {
301
+        "2020-07-21": {
302
+            startingDay: true,
303
+            endingDay: false,
304
+            color: theme.colors.primary
305
+        },
306
+        "2020-07-22": {
307
+            startingDay: false,
308
+            endingDay: false,
309
+            color: theme.colors.danger
310
+        },
311
+        "2020-07-23": {
312
+            startingDay: false,
313
+            endingDay: true,
314
+            color: theme.colors.primary
315
+        },
316
+    };
317
+    range = EquipmentBooking.getValidRange(end, start, testDevice);
318
+    expect(EquipmentBooking.generateMarkedDates(true, theme, range)).toStrictEqual(result);
319
+});

+ 0
- 1
package.json View File

@@ -21,7 +21,6 @@
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",
25 24
     "@react-native-community/masked-view": "^0.1.10",
26 25
     "@react-native-community/push-notification-ios": "^1.2.2",
27 26
     "@react-native-community/slider": "^3.0.0",

+ 8
- 28
src/components/Lists/Equipment/EquipmentListItem.js View File

@@ -5,7 +5,11 @@ import {Avatar, List, withTheme} from 'react-native-paper';
5 5
 import type {CustomTheme} from "../../../managers/ThemeManager";
6 6
 import type {Device} from "../../../screens/Amicale/Equipment/EquipmentListScreen";
7 7
 import i18n from "i18n-js";
8
-import {getTimeOnlyString, stringToDate} from "../../../utils/Planning";
8
+import {
9
+    getFirstEquipmentAvailability,
10
+    getRelativeDateString,
11
+    isEquipmentAvailable
12
+} from "../../../utils/EquipmentBooking";
9 13
 
10 14
 type Props = {
11 15
     onPress: () => void,
@@ -20,41 +24,17 @@ class EquipmentListItem extends React.Component<Props> {
20 24
         return false;
21 25
     }
22 26
 
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 27
     render() {
49 28
         const colors = this.props.theme.colors;
50 29
         const item = this.props.item;
51
-        const isAvailable = this.isAvailable();
30
+        const isAvailable = isEquipmentAvailable(item);
31
+        const firstAvailability = getFirstEquipmentAvailability(item);
52 32
         return (
53 33
             <List.Item
54 34
                 title={item.name}
55 35
                 description={isAvailable
56 36
                     ? i18n.t('equipmentScreen.bail', {cost: item.caution})
57
-                    : i18n.t('equipmentScreen.availableAt', {date: this.getDateString(item.available_at)})}
37
+                    : i18n.t('equipmentScreen.available', {date: getRelativeDateString(firstAvailability)})}
58 38
                 onPress={this.props.onPress}
59 39
                 left={(props) => <Avatar.Icon
60 40
                     {...props}

+ 4
- 0
src/managers/DateManager.js View File

@@ -49,6 +49,10 @@ export default class DateManager {
49 49
         return date.getDay() === 6 || date.getDay() === 0;
50 50
     }
51 51
 
52
+    getMonthsOfYear() {
53
+        return this.monthsOfYear;
54
+    }
55
+
52 56
     /**
53 57
      * Gets a translated string representing the given date.
54 58
      *

+ 3
- 32
src/screens/Amicale/Equipment/EquipmentListScreen.js View File

@@ -22,36 +22,9 @@ export type Device = {
22 22
     id: number,
23 23
     name: string,
24 24
     caution: number,
25
-    available_at: string,
25
+    booked_at: Array<{begin: string, end: string}>,
26 26
 };
27 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 28
 const ICON_AMICALE = require('../../../../assets/amicale.png');
56 29
 const LIST_ITEM_HEIGHT = 64;
57 30
 
@@ -104,8 +77,6 @@ class EquipmentListScreen extends React.Component<Props> {
104 77
             const fetchedData = data[0];
105 78
             if (fetchedData != null)
106 79
                 this.data = fetchedData["devices"];
107
-
108
-            this.data = TEST_DATASET; // TODO remove in prod
109 80
         }
110 81
         const {containerPaddingTop, scrollIndicatorInsetTop, onScroll} = this.props.collapsibleStack;
111 82
         return (
@@ -131,9 +102,9 @@ class EquipmentListScreen extends React.Component<Props> {
131 102
                 {...this.props}
132 103
                 requests={[
133 104
                     {
134
-                        link: 'user/profile',
105
+                        link: 'location/all',
135 106
                         params: {},
136
-                        mandatory: false,
107
+                        mandatory: true,
137 108
                     }
138 109
                 ]}
139 110
                 renderFunction={this.getScreen}

+ 102
- 229
src/screens/Amicale/Equipment/EquipmentRentScreen.js View File

@@ -1,7 +1,7 @@
1 1
 // @flow
2 2
 
3 3
 import * as React from 'react';
4
-import {Button, Caption, Card, Headline, Subheading, Text, withTheme} from 'react-native-paper';
4
+import {Button, Caption, Card, Headline, Subheading, withTheme} from 'react-native-paper';
5 5
 import {Collapsible} from "react-navigation-collapsible";
6 6
 import {withCollapsible} from "../../../utils/withCollapsible";
7 7
 import {StackNavigationProp} from "@react-navigation/stack";
@@ -11,12 +11,18 @@ import {Animated, BackHandler} from "react-native";
11 11
 import * as Animatable from "react-native-animatable";
12 12
 import {View} from "react-native-animatable";
13 13
 import i18n from "i18n-js";
14
-import {dateToString, getTimeOnlyString, stringToDate} from "../../../utils/Planning";
15 14
 import {CalendarList} from "react-native-calendars";
16
-import DateTimePicker from '@react-native-community/datetimepicker';
17 15
 import LoadingConfirmDialog from "../../../components/Dialogs/LoadingConfirmDialog";
18 16
 import ConnectionManager from "../../../managers/ConnectionManager";
19 17
 import ErrorDialog from "../../../components/Dialogs/ErrorDialog";
18
+import {
19
+    generateMarkedDates,
20
+    getFirstEquipmentAvailability,
21
+    getISODate,
22
+    getRelativeDateString,
23
+    getValidRange,
24
+    isEquipmentAvailable
25
+} from "../../../utils/EquipmentBooking";
20 26
 
21 27
 type Props = {
22 28
     navigation: StackNavigationProp,
@@ -33,7 +39,6 @@ type State = {
33 39
     dialogVisible: boolean,
34 40
     errorDialogVisible: boolean,
35 41
     markedDates: { [key: string]: { startingDay: boolean, endingDay: boolean, color: string } },
36
-    timePickerVisible: boolean,
37 42
     currentError: number,
38 43
 }
39 44
 
@@ -43,32 +48,45 @@ class EquipmentRentScreen extends React.Component<Props, State> {
43 48
         dialogVisible: false,
44 49
         errorDialogVisible: false,
45 50
         markedDates: {},
46
-        timePickerVisible: false,
47 51
         currentError: 0,
48 52
     }
49 53
 
50 54
     item: Device | null;
51
-    selectedDates: {
52
-        start: Date | null,
53
-        end: Date | null,
54
-    };
55
-
56
-    currentlySelectedDate: Date | null;
55
+    bookedDates: Array<string>;
57 56
 
58 57
     bookRef: { current: null | Animatable.View }
59 58
     canBookEquipment: boolean;
60 59
 
60
+    lockedDates: { [key: string]: { startingDay: boolean, endingDay: boolean, color: string } }
61
+
61 62
     constructor(props: Props) {
62 63
         super(props);
63 64
         this.resetSelection();
64 65
         this.bookRef = React.createRef();
65 66
         this.canBookEquipment = false;
67
+        this.bookedDates = [];
66 68
         if (this.props.route.params != null) {
67 69
             if (this.props.route.params.item != null)
68 70
                 this.item = this.props.route.params.item;
69 71
             else
70 72
                 this.item = null;
71 73
         }
74
+        const item = this.item;
75
+        if (item != null) {
76
+            this.lockedDates = {};
77
+            for (let i = 0; i < item.booked_at.length; i++) {
78
+                const range = getValidRange(new Date(item.booked_at[i].begin), new Date(item.booked_at[i].end), null);
79
+                this.lockedDates = {
80
+                    ...this.lockedDates,
81
+                    ...generateMarkedDates(
82
+                        false,
83
+                        this.props.theme,
84
+                        range
85
+                    )
86
+                };
87
+            }
88
+        }
89
+
72 90
     }
73 91
 
74 92
     /**
@@ -99,57 +117,14 @@ class EquipmentRentScreen extends React.Component<Props, State> {
99 117
      * @return {boolean}
100 118
      */
101 119
     onBackButtonPressAndroid = () => {
102
-        if (this.currentlySelectedDate != null) {
120
+        if (this.bookedDates.length > 0) {
103 121
             this.resetSelection();
104
-            this.setState({
105
-                markedDates: this.generateMarkedDates(),
106
-            });
122
+            this.updateMarkedSelection();
107 123
             return true;
108 124
         } else
109 125
             return false;
110 126
     };
111 127
 
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 128
     /**
154 129
      * Selects a new date on the calendar.
155 130
      * If both start and end dates are already selected, unselect all.
@@ -157,149 +132,50 @@ class EquipmentRentScreen extends React.Component<Props, State> {
157 132
      * @param day The day selected
158 133
      */
159 134
     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;
135
+        const selected = new Date(day.dateString);
136
+        const start = this.getBookStartDate();
137
+
138
+        if (!(this.lockedDates.hasOwnProperty(day.dateString))) {
139
+            if (start === null) {
140
+                this.updateSelectionRange(selected, selected);
141
+                this.enableBooking();
142
+            } else if (start.getTime() === selected.getTime()) {
143
+                this.resetSelection();
144
+            } else if (this.bookedDates.length === 1) {
145
+                this.updateSelectionRange(start, selected);
146
+                this.enableBooking();
169 147
             } 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 148
                 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
-            }
149
+            this.updateMarkedSelection();
212 150
         }
213 151
     }
214 152
 
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();
153
+    updateSelectionRange(start: Date, end: Date) {
154
+        this.bookedDates = getValidRange(start, end, this.item);
155
+    }
248 156
 
157
+    updateMarkedSelection() {
249 158
         this.setState({
250
-            timePickerVisible: false,
251
-            markedDates: this.generateMarkedDates(),
159
+            markedDates: generateMarkedDates(
160
+                true,
161
+                this.props.theme,
162
+                this.bookedDates
163
+            ),
252 164
         });
253 165
     }
254 166
 
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];
167
+    enableBooking() {
168
+        if (!this.canBookEquipment) {
169
+            this.showBookButton();
170
+            this.canBookEquipment = true;
171
+        }
263 172
     }
264 173
 
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;
174
+    resetSelection() {
175
+        if (this.canBookEquipment)
176
+            this.hideBookButton();
177
+        this.canBookEquipment = false;
178
+        this.bookedDates = [];
303 179
     }
304 180
 
305 181
     /**
@@ -349,15 +225,15 @@ class EquipmentRentScreen extends React.Component<Props, State> {
349 225
     onDialogAccept = () => {
350 226
         return new Promise((resolve) => {
351 227
             const item = this.item;
352
-            const start = this.selectedDates.start;
353
-            const end = this.selectedDates.end;
228
+            const start = this.getBookStartDate();
229
+            const end = this.getBookEndDate();
354 230
             if (item != null && start != null && end != null) {
355 231
                 ConnectionManager.getInstance().authenticatedRequest(
356
-                    "", // TODO set path
232
+                    "location/booking",
357 233
                     {
358
-                        "id": item.id,
359
-                        "start": dateToString(start, false),
360
-                        "end": dateToString(end, false),
234
+                        "device": item.id,
235
+                        "begin": getISODate(start),
236
+                        "end": getISODate(end),
361 237
                     })
362 238
                     .then(() => {
363 239
                         console.log("Success, replace screen");
@@ -373,20 +249,25 @@ class EquipmentRentScreen extends React.Component<Props, State> {
373 249
         });
374 250
     }
375 251
 
252
+    getBookStartDate() {
253
+        return this.bookedDates.length > 0 ? new Date(this.bookedDates[0]) : null;
254
+    }
255
+
256
+    getBookEndDate() {
257
+        const length = this.bookedDates.length;
258
+        return length > 0 ? new Date(this.bookedDates[length - 1]) : null;
259
+    }
260
+
376 261
     render() {
377 262
         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 263
 
387 264
         const item = this.item;
265
+        const start = this.getBookStartDate();
266
+        const end = this.getBookEndDate();
267
+
388 268
         if (item != null) {
389
-            const isAvailable = this.isAvailable(item);
269
+            const isAvailable = isEquipmentAvailable(item);
270
+            const firstAvailability = getFirstEquipmentAvailability(item);
390 271
             return (
391 272
                 <View style={{flex: 1}}>
392 273
                     <Animated.ScrollView
@@ -424,40 +305,32 @@ class EquipmentRentScreen extends React.Component<Props, State> {
424 305
                                     color={isAvailable ? this.props.theme.colors.success : this.props.theme.colors.primary}
425 306
                                     mode="text"
426 307
                                 >
427
-                                    {
428
-                                        isAvailable
429
-                                            ? i18n.t('equipmentScreen.available')
430
-                                            : i18n.t('equipmentScreen.availableAt', {date: this.getDateString(item.available_at)})
431
-                                    }
308
+                                    {i18n.t('equipmentScreen.available', {date: getRelativeDateString(firstAvailability)})}
432 309
                                 </Button>
433
-                                <Text style={{
310
+                                <Subheading style={{
434 311
                                     textAlign: "center",
435
-                                    marginBottom: 10
312
+                                    marginBottom: 10,
313
+                                    minHeight: 50
436 314
                                 }}>
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}
315
+                                    {
316
+                                        start == null
317
+                                            ? i18n.t('equipmentScreen.booking')
318
+                                            : end != null && start.getTime() !== end.getTime()
319
+                                            ? i18n.t('equipmentScreen.bookingPeriod', {
320
+                                                begin: getRelativeDateString(start),
321
+                                                end: getRelativeDateString(end)
322
+                                            })
323
+                                            : i18n.t('equipmentScreen.bookingDay', {
324
+                                                date: getRelativeDateString(start)
325
+                                            })
326
+                                    }
327
+
446 328
                                 </Subheading>
447 329
                             </Card.Content>
448 330
                         </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 331
                         <CalendarList
459 332
                             // Minimum date that can be selected, dates before minDate will be grayed out. Default = undefined
460
-                            minDate={this.getMinDate(item, isAvailable)}
333
+                            minDate={new Date()}
461 334
                             // Max amount of months allowed to scroll to the past. Default = 50
462 335
                             pastScrollRange={0}
463 336
                             // Max amount of months allowed to scroll to the future. Default = 50
@@ -476,7 +349,7 @@ class EquipmentRentScreen extends React.Component<Props, State> {
476 349
                             hideArrows={false}
477 350
                             // Date marking style [simple/period/multi-dot/custom]. Default = 'simple'
478 351
                             markingType={'period'}
479
-                            markedDates={this.state.markedDates}
352
+                            markedDates={{...this.lockedDates, ...this.state.markedDates}}
480 353
 
481 354
                             theme={{
482 355
                                 backgroundColor: this.props.theme.colors.agendaBackgroundColor,

+ 176
- 0
src/utils/EquipmentBooking.js View File

@@ -0,0 +1,176 @@
1
+// @flow
2
+
3
+import type {Device} from "../screens/Amicale/Equipment/EquipmentListScreen";
4
+import i18n from "i18n-js";
5
+import DateManager from "../managers/DateManager";
6
+import type {CustomTheme} from "../managers/ThemeManager";
7
+
8
+/**
9
+ * Gets the current day at midnight
10
+ *
11
+ * @returns {Date}
12
+ */
13
+export function getCurrentDay() {
14
+    let today = new Date(Date.now());
15
+    today.setUTCHours(0, 0, 0, 0);
16
+    return today;
17
+}
18
+
19
+/**
20
+ * Returns the ISO date format (without the time)
21
+ *
22
+ * @param date The date to recover the ISO format from
23
+ * @returns {*}
24
+ */
25
+export function getISODate(date: Date) {
26
+    return date.toISOString().split("T")[0];
27
+}
28
+
29
+/**
30
+ * Finds if the given equipment is available today
31
+ *
32
+ * @param item
33
+ * @returns {boolean}
34
+ */
35
+export function isEquipmentAvailable(item: Device) {
36
+    let isAvailable = true;
37
+    const today = getCurrentDay();
38
+    const dates = item.booked_at;
39
+    for (let i = 0; i < dates.length; i++) {
40
+        const start = new Date(dates[i].begin);
41
+        const end = new Date(dates[i].end);
42
+        isAvailable = today < start || today > end;
43
+        if (!isAvailable)
44
+            break;
45
+    }
46
+    return isAvailable;
47
+}
48
+
49
+/**
50
+ * Finds the first date free for booking.
51
+ *
52
+ * @param item
53
+ * @returns {Date}
54
+ */
55
+export function getFirstEquipmentAvailability(item: Device) {
56
+    let firstAvailability = getCurrentDay();
57
+    const dates = item.booked_at;
58
+    for (let i = 0; i < dates.length; i++) {
59
+        const start = new Date(dates[i].begin);
60
+        let end = new Date(dates[i].end);
61
+        end.setDate(end.getDate() + 1);
62
+        if (firstAvailability >= start)
63
+            firstAvailability = end;
64
+    }
65
+    return firstAvailability;
66
+}
67
+
68
+/**
69
+ * Gets a translated string representing the given date, relative to the current date
70
+ *
71
+ * @param date The date to translate
72
+ */
73
+export function getRelativeDateString(date: Date) {
74
+    const today = getCurrentDay();
75
+    const yearDelta = date.getUTCFullYear() - today.getUTCFullYear();
76
+    const monthDelta = date.getUTCMonth() - today.getUTCMonth();
77
+    const dayDelta = date.getUTCDate() - today.getUTCDate();
78
+    let translatedString = i18n.t('equipmentScreen.today');
79
+    if (yearDelta > 0)
80
+        translatedString = i18n.t('equipmentScreen.otherYear', {
81
+            date: date.getDate(),
82
+            month: DateManager.getInstance().getMonthsOfYear()[date.getMonth()],
83
+            year: date.getFullYear()
84
+        });
85
+    else if (monthDelta > 0)
86
+        translatedString = i18n.t('equipmentScreen.otherMonth', {
87
+            date: date.getDate(),
88
+            month: DateManager.getInstance().getMonthsOfYear()[date.getMonth()],
89
+        });
90
+    else if (dayDelta > 1)
91
+        translatedString = i18n.t('equipmentScreen.thisMonth', {
92
+            date: date.getDate(),
93
+        });
94
+    else if (dayDelta === 1)
95
+        translatedString = i18n.t('equipmentScreen.tomorrow');
96
+
97
+    return translatedString;
98
+}
99
+
100
+/**
101
+ * Gets a valid array of dates between the given start and end, for the corresponding item.
102
+ * I stops at the first booked date encountered before the end.
103
+ * It assumes the range start and end are valid.
104
+ *
105
+ * Start and End specify the range's direction.
106
+ * If start < end, it will begin at Start and stop if it encounters any booked date before reaching End.
107
+ * If start > end, it will begin at End and stop if it encounters any booked dates before reaching Start.
108
+ *
109
+ * @param start Range start
110
+ * @param end Range end
111
+ * @param item Item containing booked dates to look for
112
+ * @returns {[string]}
113
+ */
114
+export function getValidRange(start: Date, end: Date, item: Device | null) {
115
+    let direction = start <= end ? 1 : -1;
116
+    let limit = new Date(end);
117
+    limit.setDate(limit.getDate() + direction); // Limit is excluded, but we want to include range end
118
+    if (item != null) {
119
+        if (direction === 1) {
120
+            for (let i = 0; i < item.booked_at.length; i++) {
121
+                const bookLimit = new Date(item.booked_at[i].begin);
122
+                if (start < bookLimit && limit > bookLimit) {
123
+                    limit = bookLimit;
124
+                    break;
125
+                }
126
+            }
127
+        } else {
128
+            for (let i = item.booked_at.length - 1; i >= 0; i--) {
129
+                const bookLimit = new Date(item.booked_at[i].end);
130
+                if (start > bookLimit && limit < bookLimit) {
131
+                    limit = bookLimit;
132
+                    break;
133
+                }
134
+            }
135
+        }
136
+    }
137
+
138
+
139
+    let validRange = [];
140
+    let date = new Date(start);
141
+    while ((direction === 1 && date < limit) || (direction === -1 && date > limit)) {
142
+        if (direction === 1)
143
+            validRange.push(getISODate(date));
144
+        else
145
+            validRange.unshift(getISODate(date));
146
+        date.setDate(date.getDate() + direction);
147
+    }
148
+    return validRange;
149
+}
150
+
151
+/**
152
+ * Generates calendar compatible marked dates from the given array
153
+ *
154
+ *
155
+ * @param isSelection True to use user selection color, false to use disabled color
156
+ * @param theme The current App theme to get colors from
157
+ * @param range The range to mark dates for
158
+ * @returns {{}}
159
+ */
160
+export function generateMarkedDates(isSelection: boolean, theme: CustomTheme, range: Array<string>) {
161
+    let markedDates = {}
162
+    for (let i = 0; i < range.length; i++) {
163
+        const isStart = i === 0;
164
+        const isEnd = i === range.length - 1;
165
+        markedDates[range[i]] = {
166
+            startingDay: isStart,
167
+            endingDay: isEnd,
168
+            color: isSelection
169
+                ? isStart || isEnd
170
+                    ? theme.colors.primary
171
+                    : theme.colors.danger
172
+                : theme.colors.textDisabled
173
+        };
174
+    }
175
+    return markedDates;
176
+}

+ 1
- 1
src/utils/Planning.js View File

@@ -114,7 +114,7 @@ export function stringToDate(dateString: string): Date | null {
114 114
 
115 115
 /**
116 116
  * Converts a date object to a string in the format
117
- * YYYY-MM-DD HH-MM-SS
117
+ * YYYY-MM-DD HH-MM
118 118
  *
119 119
  * @param date The date object to convert
120 120
  * @param isUTC Whether to treat the date as UTC

+ 8
- 5
translations/en.json View File

@@ -452,12 +452,15 @@
452 452
     "title": "Equipment booking",
453 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 454
     "bail": "Bail: %{cost}€",
455
-    "availableAt": "Available at: %{date}",
456
-    "available": "Available!",
455
+    "available": "Available %{date}",
456
+    "today": "today",
457
+    "tomorrow": "tomorrow",
458
+    "thisMonth": "the %{date}",
459
+    "otherMonth": "the %{date} of %{month}",
460
+    "otherYear": "the %{date} of %{month} %{year}",
461
+    "bookingDay": "Booked for %{date}",
462
+    "bookingPeriod": "Booked from %{begin} to %{end}",
457 463
     "booking": "Click on the calendar to set the start and end dates",
458
-    "startDate": "Start: ",
459
-    "endDate": "End: ",
460
-    "notSet": "Not set",
461 464
     "bookButton": "Book selected dates",
462 465
     "dialogTitle": "Confirm booking?",
463 466
     "dialogTitleLoading": "Sending your booking...",

Loading…
Cancel
Save