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

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
   "dependencies": {
21
   "dependencies": {
22
     "@nartc/react-native-barcode-mask": "^1.2.0",
22
     "@nartc/react-native-barcode-mask": "^1.2.0",
23
     "@react-native-community/async-storage": "^1.11.0",
23
     "@react-native-community/async-storage": "^1.11.0",
24
-    "@react-native-community/datetimepicker": "^2.6.0",
25
     "@react-native-community/masked-view": "^0.1.10",
24
     "@react-native-community/masked-view": "^0.1.10",
26
     "@react-native-community/push-notification-ios": "^1.2.2",
25
     "@react-native-community/push-notification-ios": "^1.2.2",
27
     "@react-native-community/slider": "^3.0.0",
26
     "@react-native-community/slider": "^3.0.0",

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

5
 import type {CustomTheme} from "../../../managers/ThemeManager";
5
 import type {CustomTheme} from "../../../managers/ThemeManager";
6
 import type {Device} from "../../../screens/Amicale/Equipment/EquipmentListScreen";
6
 import type {Device} from "../../../screens/Amicale/Equipment/EquipmentListScreen";
7
 import i18n from "i18n-js";
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
 type Props = {
14
 type Props = {
11
     onPress: () => void,
15
     onPress: () => void,
20
         return false;
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
     render() {
27
     render() {
49
         const colors = this.props.theme.colors;
28
         const colors = this.props.theme.colors;
50
         const item = this.props.item;
29
         const item = this.props.item;
51
-        const isAvailable = this.isAvailable();
30
+        const isAvailable = isEquipmentAvailable(item);
31
+        const firstAvailability = getFirstEquipmentAvailability(item);
52
         return (
32
         return (
53
             <List.Item
33
             <List.Item
54
                 title={item.name}
34
                 title={item.name}
55
                 description={isAvailable
35
                 description={isAvailable
56
                     ? i18n.t('equipmentScreen.bail', {cost: item.caution})
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
                 onPress={this.props.onPress}
38
                 onPress={this.props.onPress}
59
                 left={(props) => <Avatar.Icon
39
                 left={(props) => <Avatar.Icon
60
                     {...props}
40
                     {...props}

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

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

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

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

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

1
 // @flow
1
 // @flow
2
 
2
 
3
 import * as React from 'react';
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
 import {Collapsible} from "react-navigation-collapsible";
5
 import {Collapsible} from "react-navigation-collapsible";
6
 import {withCollapsible} from "../../../utils/withCollapsible";
6
 import {withCollapsible} from "../../../utils/withCollapsible";
7
 import {StackNavigationProp} from "@react-navigation/stack";
7
 import {StackNavigationProp} from "@react-navigation/stack";
11
 import * as Animatable from "react-native-animatable";
11
 import * as Animatable from "react-native-animatable";
12
 import {View} from "react-native-animatable";
12
 import {View} from "react-native-animatable";
13
 import i18n from "i18n-js";
13
 import i18n from "i18n-js";
14
-import {dateToString, getTimeOnlyString, stringToDate} from "../../../utils/Planning";
15
 import {CalendarList} from "react-native-calendars";
14
 import {CalendarList} from "react-native-calendars";
16
-import DateTimePicker from '@react-native-community/datetimepicker';
17
 import LoadingConfirmDialog from "../../../components/Dialogs/LoadingConfirmDialog";
15
 import LoadingConfirmDialog from "../../../components/Dialogs/LoadingConfirmDialog";
18
 import ConnectionManager from "../../../managers/ConnectionManager";
16
 import ConnectionManager from "../../../managers/ConnectionManager";
19
 import ErrorDialog from "../../../components/Dialogs/ErrorDialog";
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
 type Props = {
27
 type Props = {
22
     navigation: StackNavigationProp,
28
     navigation: StackNavigationProp,
33
     dialogVisible: boolean,
39
     dialogVisible: boolean,
34
     errorDialogVisible: boolean,
40
     errorDialogVisible: boolean,
35
     markedDates: { [key: string]: { startingDay: boolean, endingDay: boolean, color: string } },
41
     markedDates: { [key: string]: { startingDay: boolean, endingDay: boolean, color: string } },
36
-    timePickerVisible: boolean,
37
     currentError: number,
42
     currentError: number,
38
 }
43
 }
39
 
44
 
43
         dialogVisible: false,
48
         dialogVisible: false,
44
         errorDialogVisible: false,
49
         errorDialogVisible: false,
45
         markedDates: {},
50
         markedDates: {},
46
-        timePickerVisible: false,
47
         currentError: 0,
51
         currentError: 0,
48
     }
52
     }
49
 
53
 
50
     item: Device | null;
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
     bookRef: { current: null | Animatable.View }
57
     bookRef: { current: null | Animatable.View }
59
     canBookEquipment: boolean;
58
     canBookEquipment: boolean;
60
 
59
 
60
+    lockedDates: { [key: string]: { startingDay: boolean, endingDay: boolean, color: string } }
61
+
61
     constructor(props: Props) {
62
     constructor(props: Props) {
62
         super(props);
63
         super(props);
63
         this.resetSelection();
64
         this.resetSelection();
64
         this.bookRef = React.createRef();
65
         this.bookRef = React.createRef();
65
         this.canBookEquipment = false;
66
         this.canBookEquipment = false;
67
+        this.bookedDates = [];
66
         if (this.props.route.params != null) {
68
         if (this.props.route.params != null) {
67
             if (this.props.route.params.item != null)
69
             if (this.props.route.params.item != null)
68
                 this.item = this.props.route.params.item;
70
                 this.item = this.props.route.params.item;
69
             else
71
             else
70
                 this.item = null;
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
      * @return {boolean}
117
      * @return {boolean}
100
      */
118
      */
101
     onBackButtonPressAndroid = () => {
119
     onBackButtonPressAndroid = () => {
102
-        if (this.currentlySelectedDate != null) {
120
+        if (this.bookedDates.length > 0) {
103
             this.resetSelection();
121
             this.resetSelection();
104
-            this.setState({
105
-                markedDates: this.generateMarkedDates(),
106
-            });
122
+            this.updateMarkedSelection();
107
             return true;
123
             return true;
108
         } else
124
         } else
109
             return false;
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
      * Selects a new date on the calendar.
129
      * Selects a new date on the calendar.
155
      * If both start and end dates are already selected, unselect all.
130
      * If both start and end dates are already selected, unselect all.
157
      * @param day The day selected
132
      * @param day The day selected
158
      */
133
      */
159
     selectNewDate = (day: { dateString: string, day: number, month: number, timestamp: number, year: number }) => {
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
             } else
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
                 this.resetSelection();
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
         this.setState({
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
     onDialogAccept = () => {
225
     onDialogAccept = () => {
350
         return new Promise((resolve) => {
226
         return new Promise((resolve) => {
351
             const item = this.item;
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
             if (item != null && start != null && end != null) {
230
             if (item != null && start != null && end != null) {
355
                 ConnectionManager.getInstance().authenticatedRequest(
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
                     .then(() => {
238
                     .then(() => {
363
                         console.log("Success, replace screen");
239
                         console.log("Success, replace screen");
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
     render() {
261
     render() {
377
         const {containerPaddingTop, scrollIndicatorInsetTop, onScroll} = this.props.collapsibleStack;
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
         const item = this.item;
264
         const item = this.item;
265
+        const start = this.getBookStartDate();
266
+        const end = this.getBookEndDate();
267
+
388
         if (item != null) {
268
         if (item != null) {
389
-            const isAvailable = this.isAvailable(item);
269
+            const isAvailable = isEquipmentAvailable(item);
270
+            const firstAvailability = getFirstEquipmentAvailability(item);
390
             return (
271
             return (
391
                 <View style={{flex: 1}}>
272
                 <View style={{flex: 1}}>
392
                     <Animated.ScrollView
273
                     <Animated.ScrollView
424
                                     color={isAvailable ? this.props.theme.colors.success : this.props.theme.colors.primary}
305
                                     color={isAvailable ? this.props.theme.colors.success : this.props.theme.colors.primary}
425
                                     mode="text"
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
                                 </Button>
309
                                 </Button>
433
-                                <Text style={{
310
+                                <Subheading style={{
434
                                     textAlign: "center",
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
                                 </Subheading>
328
                                 </Subheading>
447
                             </Card.Content>
329
                             </Card.Content>
448
                         </Card>
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
                         <CalendarList
331
                         <CalendarList
459
                             // Minimum date that can be selected, dates before minDate will be grayed out. Default = undefined
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
                             // Max amount of months allowed to scroll to the past. Default = 50
334
                             // Max amount of months allowed to scroll to the past. Default = 50
462
                             pastScrollRange={0}
335
                             pastScrollRange={0}
463
                             // Max amount of months allowed to scroll to the future. Default = 50
336
                             // Max amount of months allowed to scroll to the future. Default = 50
476
                             hideArrows={false}
349
                             hideArrows={false}
477
                             // Date marking style [simple/period/multi-dot/custom]. Default = 'simple'
350
                             // Date marking style [simple/period/multi-dot/custom]. Default = 'simple'
478
                             markingType={'period'}
351
                             markingType={'period'}
479
-                            markedDates={this.state.markedDates}
352
+                            markedDates={{...this.lockedDates, ...this.state.markedDates}}
480
 
353
 
481
                             theme={{
354
                             theme={{
482
                                 backgroundColor: this.props.theme.colors.agendaBackgroundColor,
355
                                 backgroundColor: this.props.theme.colors.agendaBackgroundColor,

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

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
 
114
 
115
 /**
115
 /**
116
  * Converts a date object to a string in the format
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
  * @param date The date object to convert
119
  * @param date The date object to convert
120
  * @param isUTC Whether to treat the date as UTC
120
  * @param isUTC Whether to treat the date as UTC

+ 8
- 5
translations/en.json View File

452
     "title": "Equipment booking",
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.",
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}€",
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
     "booking": "Click on the calendar to set the start and end dates",
463
     "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",
464
     "bookButton": "Book selected dates",
462
     "dialogTitle": "Confirm booking?",
465
     "dialogTitle": "Confirm booking?",
463
     "dialogTitleLoading": "Sending your booking...",
466
     "dialogTitleLoading": "Sending your booking...",

Loading…
Cancel
Save