diff --git a/__tests__/utils/EquipmentBooking.test.js b/__tests__/utils/EquipmentBooking.test.js new file mode 100644 index 0000000..0237a6a --- /dev/null +++ b/__tests__/utils/EquipmentBooking.test.js @@ -0,0 +1,319 @@ +import React from 'react'; +import * as EquipmentBooking from "../../src/utils/EquipmentBooking"; +import i18n from "i18n-js"; + +test('getISODate', () => { + let date = new Date("2020-03-05 12:00"); + expect(EquipmentBooking.getISODate(date)).toBe("2020-03-05"); + date = new Date("2020-03-05"); + expect(EquipmentBooking.getISODate(date)).toBe("2020-03-05"); + date = new Date("2020-03-05 00:00"); // Treated as local time + expect(EquipmentBooking.getISODate(date)).toBe("2020-03-04"); // Treated as UTC +}); + +test('getCurrentDay', () => { + jest.spyOn(Date, 'now') + .mockImplementation(() => + new Date('2020-01-14 14:50:35').getTime() + ); + expect(EquipmentBooking.getCurrentDay().getTime()).toBe(new Date("2020-01-14").getTime()); +}); + +test('isEquipmentAvailable', () => { + jest.spyOn(Date, 'now') + .mockImplementation(() => + new Date('2020-07-09').getTime() + ); + let testDevice = { + id: 1, + name: "Petit barbecue", + caution: 100, + booked_at: [{begin: "2020-07-07", end: "2020-07-10"}] + }; + expect(EquipmentBooking.isEquipmentAvailable(testDevice)).toBeFalse(); + + testDevice.booked_at = [{begin: "2020-07-07", end: "2020-07-09"}]; + expect(EquipmentBooking.isEquipmentAvailable(testDevice)).toBeFalse(); + + testDevice.booked_at = [{begin: "2020-07-09", end: "2020-07-10"}]; + expect(EquipmentBooking.isEquipmentAvailable(testDevice)).toBeFalse(); + + testDevice.booked_at = [ + {begin: "2020-07-07", end: "2020-07-8"}, + {begin: "2020-07-10", end: "2020-07-12"}, + ]; + expect(EquipmentBooking.isEquipmentAvailable(testDevice)).toBeTrue(); +}); + +test('getFirstEquipmentAvailability', () => { + jest.spyOn(Date, 'now') + .mockImplementation(() => + new Date('2020-07-09').getTime() + ); + let testDevice = { + id: 1, + name: "Petit barbecue", + caution: 100, + booked_at: [{begin: "2020-07-07", end: "2020-07-10"}] + }; + expect(EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime()).toBe(new Date("2020-07-11").getTime()); + testDevice.booked_at = [{begin: "2020-07-07", end: "2020-07-09"}]; + expect(EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime()).toBe(new Date("2020-07-10").getTime()); + testDevice.booked_at = [ + {begin: "2020-07-07", end: "2020-07-09"}, + {begin: "2020-07-10", end: "2020-07-16"}, + ]; + expect(EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime()).toBe(new Date("2020-07-17").getTime()); + testDevice.booked_at = [ + {begin: "2020-07-07", end: "2020-07-09"}, + {begin: "2020-07-10", end: "2020-07-12"}, + {begin: "2020-07-14", end: "2020-07-16"}, + ]; + expect(EquipmentBooking.getFirstEquipmentAvailability(testDevice).getTime()).toBe(new Date("2020-07-13").getTime()); +}); + +test('getRelativeDateString', () => { + jest.spyOn(Date, 'now') + .mockImplementation(() => + new Date('2020-07-09').getTime() + ); + jest.spyOn(i18n, 't') + .mockImplementation((translationString: string) => { + const prefix = "equipmentScreen."; + if (translationString === prefix + "otherYear") + return "0"; + else if (translationString === prefix + "otherMonth") + return "1"; + else if (translationString === prefix + "thisMonth") + return "2"; + else if (translationString === prefix + "tomorrow") + return "3"; + else if (translationString === prefix + "today") + return "4"; + else + return null; + } + ); + expect(EquipmentBooking.getRelativeDateString(new Date("2020-07-09"))).toBe("4"); + expect(EquipmentBooking.getRelativeDateString(new Date("2020-07-10"))).toBe("3"); + expect(EquipmentBooking.getRelativeDateString(new Date("2020-07-11"))).toBe("2"); + expect(EquipmentBooking.getRelativeDateString(new Date("2020-07-30"))).toBe("2"); + expect(EquipmentBooking.getRelativeDateString(new Date("2020-08-30"))).toBe("1"); + expect(EquipmentBooking.getRelativeDateString(new Date("2020-11-10"))).toBe("1"); + expect(EquipmentBooking.getRelativeDateString(new Date("2021-11-10"))).toBe("0"); +}); + +test('getValidRange', () => { + let testDevice = { + id: 1, + name: "Petit barbecue", + caution: 100, + booked_at: [{begin: "2020-07-07", end: "2020-07-10"}] + }; + let start = new Date("2020-07-11"); + let end = new Date("2020-07-15"); + let result = [ + "2020-07-11", + "2020-07-12", + "2020-07-13", + "2020-07-14", + "2020-07-15", + ]; + expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual(result); + testDevice.booked_at = [ + {begin: "2020-07-07", end: "2020-07-10"}, + {begin: "2020-07-13", end: "2020-07-15"}, + ]; + result = [ + "2020-07-11", + "2020-07-12", + ]; + expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual(result); + + testDevice.booked_at = [{begin: "2020-07-12", end: "2020-07-13"}]; + result = ["2020-07-11"]; + expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual(result); + testDevice.booked_at = [{begin: "2020-07-07", end: "2020-07-12"},]; + result = [ + "2020-07-13", + "2020-07-14", + "2020-07-15", + ]; + expect(EquipmentBooking.getValidRange(end, start, testDevice)).toStrictEqual(result); + start = new Date("2020-07-14"); + end = new Date("2020-07-14"); + result = [ + "2020-07-14", + ]; + expect(EquipmentBooking.getValidRange(start, start, testDevice)).toStrictEqual(result); + expect(EquipmentBooking.getValidRange(end, start, testDevice)).toStrictEqual(result); + expect(EquipmentBooking.getValidRange(start, end, null)).toStrictEqual(result); + + start = new Date("2020-07-14"); + end = new Date("2020-07-17"); + result = [ + "2020-07-14", + "2020-07-15", + "2020-07-16", + "2020-07-17", + ]; + expect(EquipmentBooking.getValidRange(start, end, null)).toStrictEqual(result); + + testDevice.booked_at = [{begin: "2020-07-17", end: "2020-07-17"}]; + result = [ + "2020-07-14", + "2020-07-15", + "2020-07-16", + ]; + expect(EquipmentBooking.getValidRange(start, end, testDevice)).toStrictEqual(result); + + testDevice.booked_at = [ + {begin: "2020-07-12", end: "2020-07-13"}, + {begin: "2020-07-15", end: "2020-07-20"}, + ]; + start = new Date("2020-07-11"); + end = new Date("2020-07-23"); + result = [ + "2020-07-21", + "2020-07-22", + "2020-07-23", + ]; + expect(EquipmentBooking.getValidRange(end, start, testDevice)).toStrictEqual(result); +}); + +test('generateMarkedDates', () => { + let theme = { + colors: { + primary: "primary", + danger: "primary", + textDisabled: "primary", + } + } + let testDevice = { + id: 1, + name: "Petit barbecue", + caution: 100, + booked_at: [{begin: "2020-07-07", end: "2020-07-10"}] + }; + let start = new Date("2020-07-11"); + let end = new Date("2020-07-13"); + let range = EquipmentBooking.getValidRange(start, end, testDevice); + let result = { + "2020-07-11": { + startingDay: true, + endingDay: false, + color: theme.colors.primary + }, + "2020-07-12": { + startingDay: false, + endingDay: false, + color: theme.colors.danger + }, + "2020-07-13": { + startingDay: false, + endingDay: true, + color: theme.colors.primary + }, + }; + expect(EquipmentBooking.generateMarkedDates(true, theme, range)).toStrictEqual(result); + result = { + "2020-07-11": { + startingDay: true, + endingDay: false, + color: theme.colors.textDisabled + }, + "2020-07-12": { + startingDay: false, + endingDay: false, + color: theme.colors.textDisabled + }, + "2020-07-13": { + startingDay: false, + endingDay: true, + color: theme.colors.textDisabled + }, + }; + expect(EquipmentBooking.generateMarkedDates(false, theme, range)).toStrictEqual(result); + result = { + "2020-07-11": { + startingDay: true, + endingDay: false, + color: theme.colors.textDisabled + }, + "2020-07-12": { + startingDay: false, + endingDay: false, + color: theme.colors.textDisabled + }, + "2020-07-13": { + startingDay: false, + endingDay: true, + color: theme.colors.textDisabled + }, + }; + range = EquipmentBooking.getValidRange(end, start, testDevice); + expect(EquipmentBooking.generateMarkedDates(false, theme, range)).toStrictEqual(result); + + testDevice.booked_at = [{begin: "2020-07-13", end: "2020-07-15"},]; + result = { + "2020-07-11": { + startingDay: true, + endingDay: false, + color: theme.colors.primary + }, + "2020-07-12": { + startingDay: false, + endingDay: true, + color: theme.colors.primary + }, + }; + range = EquipmentBooking.getValidRange(start, end, testDevice); + expect(EquipmentBooking.generateMarkedDates(true, theme, range)).toStrictEqual(result); + + testDevice.booked_at = [{begin: "2020-07-12", end: "2020-07-13"},]; + result = { + "2020-07-11": { + startingDay: true, + endingDay: true, + color: theme.colors.primary + }, + }; + range = EquipmentBooking.getValidRange(start, end, testDevice); + expect(EquipmentBooking.generateMarkedDates(true, theme, range)).toStrictEqual(result); + + testDevice.booked_at = [ + {begin: "2020-07-12", end: "2020-07-13"}, + {begin: "2020-07-15", end: "2020-07-20"}, + ]; + start = new Date("2020-07-11"); + end = new Date("2020-07-23"); + result = { + "2020-07-11": { + startingDay: true, + endingDay: true, + color: theme.colors.primary + }, + }; + range = EquipmentBooking.getValidRange(start, end, testDevice); + expect(EquipmentBooking.generateMarkedDates(true, theme, range)).toStrictEqual(result); + + result = { + "2020-07-21": { + startingDay: true, + endingDay: false, + color: theme.colors.primary + }, + "2020-07-22": { + startingDay: false, + endingDay: false, + color: theme.colors.danger + }, + "2020-07-23": { + startingDay: false, + endingDay: true, + color: theme.colors.primary + }, + }; + range = EquipmentBooking.getValidRange(end, start, testDevice); + expect(EquipmentBooking.generateMarkedDates(true, theme, range)).toStrictEqual(result); +}); diff --git a/package.json b/package.json index 2cdf674..2302ec1 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,6 @@ "dependencies": { "@nartc/react-native-barcode-mask": "^1.2.0", "@react-native-community/async-storage": "^1.11.0", - "@react-native-community/datetimepicker": "^2.6.0", "@react-native-community/masked-view": "^0.1.10", "@react-native-community/push-notification-ios": "^1.2.2", "@react-native-community/slider": "^3.0.0", diff --git a/src/components/Lists/Equipment/EquipmentListItem.js b/src/components/Lists/Equipment/EquipmentListItem.js index 78dafa8..017956b 100644 --- a/src/components/Lists/Equipment/EquipmentListItem.js +++ b/src/components/Lists/Equipment/EquipmentListItem.js @@ -5,7 +5,11 @@ import {Avatar, List, withTheme} from 'react-native-paper'; import type {CustomTheme} from "../../../managers/ThemeManager"; import type {Device} from "../../../screens/Amicale/Equipment/EquipmentListScreen"; import i18n from "i18n-js"; -import {getTimeOnlyString, stringToDate} from "../../../utils/Planning"; +import { + getFirstEquipmentAvailability, + getRelativeDateString, + isEquipmentAvailable +} from "../../../utils/EquipmentBooking"; type Props = { onPress: () => void, @@ -20,41 +24,17 @@ class EquipmentListItem extends React.Component { return false; } - isAvailable() { - const availableDate = stringToDate(this.props.item.available_at); - return availableDate != null && availableDate < new Date(); - } - - /** - * Gets the string representation of the given date. - * - * If the given date is the same day as today, only return the tile. - * Otherwise, return the full date. - * - * @param dateString The string representation of the wanted date - * @returns {string} - */ - getDateString(dateString: string): string { - const today = new Date(); - const date = stringToDate(dateString); - if (date != null && today.getDate() === date.getDate()) { - const str = getTimeOnlyString(dateString); - return str != null ? str : ""; - } else - return dateString; - } - - render() { const colors = this.props.theme.colors; const item = this.props.item; - const isAvailable = this.isAvailable(); + const isAvailable = isEquipmentAvailable(item); + const firstAvailability = getFirstEquipmentAvailability(item); return ( , }; -const TEST_DATASET = [ - { - id: 1, - name: "Petit barbecue", - caution: 100, - available_at: "2020-07-07 21:12" - }, - { - id: 2, - name: "Grand barbecue", - caution: 100, - available_at: "2020-07-08 21:12" - }, - { - id: 3, - name: "Appareil à fondue", - caution: 100, - available_at: "2020-07-09 14:12" - }, - { - id: 4, - name: "Appareil à croque-monsieur", - caution: 100, - available_at: "2020-07-10 12:12" - } -] - const ICON_AMICALE = require('../../../../assets/amicale.png'); const LIST_ITEM_HEIGHT = 64; @@ -104,8 +77,6 @@ class EquipmentListScreen extends React.Component { const fetchedData = data[0]; if (fetchedData != null) this.data = fetchedData["devices"]; - - this.data = TEST_DATASET; // TODO remove in prod } const {containerPaddingTop, scrollIndicatorInsetTop, onScroll} = this.props.collapsibleStack; return ( @@ -131,9 +102,9 @@ class EquipmentListScreen extends React.Component { {...this.props} requests={[ { - link: 'user/profile', + link: 'location/all', params: {}, - mandatory: false, + mandatory: true, } ]} renderFunction={this.getScreen} diff --git a/src/screens/Amicale/Equipment/EquipmentRentScreen.js b/src/screens/Amicale/Equipment/EquipmentRentScreen.js index 08a9b2f..c107629 100644 --- a/src/screens/Amicale/Equipment/EquipmentRentScreen.js +++ b/src/screens/Amicale/Equipment/EquipmentRentScreen.js @@ -1,7 +1,7 @@ // @flow import * as React from 'react'; -import {Button, Caption, Card, Headline, Subheading, Text, withTheme} from 'react-native-paper'; +import {Button, Caption, Card, Headline, Subheading, withTheme} from 'react-native-paper'; import {Collapsible} from "react-navigation-collapsible"; import {withCollapsible} from "../../../utils/withCollapsible"; import {StackNavigationProp} from "@react-navigation/stack"; @@ -11,12 +11,18 @@ import {Animated, BackHandler} from "react-native"; import * as Animatable from "react-native-animatable"; import {View} from "react-native-animatable"; import i18n from "i18n-js"; -import {dateToString, getTimeOnlyString, stringToDate} from "../../../utils/Planning"; import {CalendarList} from "react-native-calendars"; -import DateTimePicker from '@react-native-community/datetimepicker'; import LoadingConfirmDialog from "../../../components/Dialogs/LoadingConfirmDialog"; import ConnectionManager from "../../../managers/ConnectionManager"; import ErrorDialog from "../../../components/Dialogs/ErrorDialog"; +import { + generateMarkedDates, + getFirstEquipmentAvailability, + getISODate, + getRelativeDateString, + getValidRange, + isEquipmentAvailable +} from "../../../utils/EquipmentBooking"; type Props = { navigation: StackNavigationProp, @@ -33,7 +39,6 @@ type State = { dialogVisible: boolean, errorDialogVisible: boolean, markedDates: { [key: string]: { startingDay: boolean, endingDay: boolean, color: string } }, - timePickerVisible: boolean, currentError: number, } @@ -43,32 +48,45 @@ class EquipmentRentScreen extends React.Component { dialogVisible: false, errorDialogVisible: false, markedDates: {}, - timePickerVisible: false, currentError: 0, } item: Device | null; - selectedDates: { - start: Date | null, - end: Date | null, - }; - - currentlySelectedDate: Date | null; + bookedDates: Array; bookRef: { current: null | Animatable.View } canBookEquipment: boolean; + lockedDates: { [key: string]: { startingDay: boolean, endingDay: boolean, color: string } } + constructor(props: Props) { super(props); this.resetSelection(); this.bookRef = React.createRef(); this.canBookEquipment = false; + this.bookedDates = []; if (this.props.route.params != null) { if (this.props.route.params.item != null) this.item = this.props.route.params.item; else this.item = null; } + const item = this.item; + if (item != null) { + this.lockedDates = {}; + for (let i = 0; i < item.booked_at.length; i++) { + const range = getValidRange(new Date(item.booked_at[i].begin), new Date(item.booked_at[i].end), null); + this.lockedDates = { + ...this.lockedDates, + ...generateMarkedDates( + false, + this.props.theme, + range + ) + }; + } + } + } /** @@ -99,57 +117,14 @@ class EquipmentRentScreen extends React.Component { * @return {boolean} */ onBackButtonPressAndroid = () => { - if (this.currentlySelectedDate != null) { + if (this.bookedDates.length > 0) { this.resetSelection(); - this.setState({ - markedDates: this.generateMarkedDates(), - }); + this.updateMarkedSelection(); return true; } else return false; }; - isAvailable(item: Device) { - const availableDate = stringToDate(item.available_at); - return availableDate != null && availableDate < new Date(); - } - - /** - * Gets the string representation of the given date. - * - * If the given date is the same day as today, only return the tile. - * Otherwise, return the full date. - * - * @param dateString The string representation of the wanted date - * @returns {string} - */ - getDateString(dateString: string): string { - const today = new Date(); - const date = stringToDate(dateString); - if (date != null && today.getDate() === date.getDate()) { - const str = getTimeOnlyString(dateString); - return str != null ? str : ""; - } else - return dateString; - } - - /** - * Gets the minimum date for renting equipment - * - * @param item The item to rent - * @param isAvailable True is it is available right now - * @returns {Date} - */ - getMinDate(item: Device, isAvailable: boolean) { - let date = new Date(); - if (isAvailable) - return date; - else { - const limit = stringToDate(item.available_at) - return limit != null ? limit : date; - } - } - /** * Selects a new date on the calendar. * If both start and end dates are already selected, unselect all. @@ -157,29 +132,42 @@ class EquipmentRentScreen extends React.Component { * @param day The day selected */ selectNewDate = (day: { dateString: string, day: number, month: number, timestamp: number, year: number }) => { - this.currentlySelectedDate = new Date(day.dateString); + const selected = new Date(day.dateString); + const start = this.getBookStartDate(); - if (!this.canBookEquipment) { - const start = this.selectedDates.start; - if (start == null) - this.selectedDates.start = this.currentlySelectedDate; - else if (this.currentlySelectedDate < start) { - this.selectedDates.end = start; - this.selectedDates.start = this.currentlySelectedDate; + if (!(this.lockedDates.hasOwnProperty(day.dateString))) { + if (start === null) { + this.updateSelectionRange(selected, selected); + this.enableBooking(); + } else if (start.getTime() === selected.getTime()) { + this.resetSelection(); + } else if (this.bookedDates.length === 1) { + this.updateSelectionRange(start, selected); + this.enableBooking(); } else - this.selectedDates.end = this.currentlySelectedDate; - } else - this.resetSelection(); + this.resetSelection(); + this.updateMarkedSelection(); + } + } - if (this.selectedDates.start != null) { - this.setState({ - markedDates: this.generateMarkedDates(), - timePickerVisible: true, - }); - } else { - this.setState({ - markedDates: this.generateMarkedDates(), - }); + updateSelectionRange(start: Date, end: Date) { + this.bookedDates = getValidRange(start, end, this.item); + } + + updateMarkedSelection() { + this.setState({ + markedDates: generateMarkedDates( + true, + this.props.theme, + this.bookedDates + ), + }); + } + + enableBooking() { + if (!this.canBookEquipment) { + this.showBookButton(); + this.canBookEquipment = true; } } @@ -187,119 +175,7 @@ class EquipmentRentScreen extends React.Component { if (this.canBookEquipment) this.hideBookButton(); this.canBookEquipment = false; - this.selectedDates = {start: null, end: null}; - this.currentlySelectedDate = null; - } - - /** - * Deselect the currently selected date - */ - deselectCurrentDate() { - let currentlySelectedDate = this.currentlySelectedDate; - const start = this.selectedDates.start; - const end = this.selectedDates.end; - if (currentlySelectedDate != null && start != null) { - if (currentlySelectedDate === start && end === null) - this.resetSelection(); - else if (end != null && currentlySelectedDate === end) { - this.currentlySelectedDate = start; - this.selectedDates.end = null; - } else if (currentlySelectedDate === start) { - this.currentlySelectedDate = end; - this.selectedDates.start = this.selectedDates.end; - this.selectedDates.end = null; - } - } - } - - /** - * Saves the selected time to the currently selected date. - * If no the time selection was canceled, cancels the current selecction - * - * @param event The click event - * @param date The date selected - */ - onTimeChange = (event: { nativeEvent: { timestamp: number }, type: string }, date: Date) => { - let currentDate = this.currentlySelectedDate; - const item = this.item; - if (item != null && event.type === "set" && currentDate != null) { - currentDate.setHours(date.getHours()); - currentDate.setMinutes(date.getMinutes()); - - const isAvailable = this.isAvailable(item); - let limit = this.getMinDate(item, isAvailable); - // Prevent selecting a date before now - if (this.getISODate(currentDate) === this.getISODate(limit) && currentDate < limit) { - currentDate.setHours(limit.getHours()); - currentDate.setMinutes(limit.getMinutes()); - } - - if (this.selectedDates.start != null && this.selectedDates.end != null) { - if (this.selectedDates.start > this.selectedDates.end) { - const temp = this.selectedDates.start; - this.selectedDates.start = this.selectedDates.end; - this.selectedDates.end = temp; - } - this.canBookEquipment = true; - this.showBookButton(); - } - } else - this.deselectCurrentDate(); - - this.setState({ - timePickerVisible: false, - markedDates: this.generateMarkedDates(), - }); - } - - /** - * Returns the ISO date format (without the time) - * - * @param date The date to recover the ISO format from - * @returns {*} - */ - getISODate(date: Date) { - return date.toISOString().split("T")[0]; - } - - /** - * Generates the object containing all marked dates between the start and end dates selected - * - * @returns {{}} - */ - generateMarkedDates() { - let markedDates = {} - const start = this.selectedDates.start; - const end = this.selectedDates.end; - if (start != null) { - const startISODate = this.getISODate(start); - if (end != null && this.getISODate(end) !== startISODate) { - markedDates[startISODate] = { - startingDay: true, - endingDay: false, - color: this.props.theme.colors.primary - }; - markedDates[this.getISODate(end)] = { - startingDay: false, - endingDay: true, - color: this.props.theme.colors.primary - }; - let date = new Date(start); - date.setDate(date.getDate() + 1); - while (date < end && this.getISODate(date) !== this.getISODate(end)) { - markedDates[this.getISODate(date)] = - {startingDay: false, endingDay: false, color: this.props.theme.colors.danger}; - date.setDate(date.getDate() + 1); - } - } else { - markedDates[startISODate] = { - startingDay: true, - endingDay: true, - color: this.props.theme.colors.primary - }; - } - } - return markedDates; + this.bookedDates = []; } /** @@ -349,15 +225,15 @@ class EquipmentRentScreen extends React.Component { onDialogAccept = () => { return new Promise((resolve) => { const item = this.item; - const start = this.selectedDates.start; - const end = this.selectedDates.end; + const start = this.getBookStartDate(); + const end = this.getBookEndDate(); if (item != null && start != null && end != null) { ConnectionManager.getInstance().authenticatedRequest( - "", // TODO set path + "location/booking", { - "id": item.id, - "start": dateToString(start, false), - "end": dateToString(end, false), + "device": item.id, + "begin": getISODate(start), + "end": getISODate(end), }) .then(() => { console.log("Success, replace screen"); @@ -373,20 +249,25 @@ class EquipmentRentScreen extends React.Component { }); } + getBookStartDate() { + return this.bookedDates.length > 0 ? new Date(this.bookedDates[0]) : null; + } + + getBookEndDate() { + const length = this.bookedDates.length; + return length > 0 ? new Date(this.bookedDates[length - 1]) : null; + } + render() { const {containerPaddingTop, scrollIndicatorInsetTop, onScroll} = this.props.collapsibleStack; - let startString = {i18n.t('equipmentScreen.notSet')}; - let endString = {i18n.t('equipmentScreen.notSet')}; - const start = this.selectedDates.start; - const end = this.selectedDates.end; - if (start != null) - startString = dateToString(start, false); - if (end != null) - endString = dateToString(end, false); const item = this.item; + const start = this.getBookStartDate(); + const end = this.getBookEndDate(); + if (item != null) { - const isAvailable = this.isAvailable(item); + const isAvailable = isEquipmentAvailable(item); + const firstAvailability = getFirstEquipmentAvailability(item); return ( { color={isAvailable ? this.props.theme.colors.success : this.props.theme.colors.primary} mode="text" > - { - isAvailable - ? i18n.t('equipmentScreen.available') - : i18n.t('equipmentScreen.availableAt', {date: this.getDateString(item.available_at)}) - } + {i18n.t('equipmentScreen.available', {date: getRelativeDateString(firstAvailability)})} - - {i18n.t('equipmentScreen.booking')} - - - {i18n.t('equipmentScreen.startDate')} - {startString} - - - {i18n.t('equipmentScreen.endDate')} - {endString} + { + start == null + ? i18n.t('equipmentScreen.booking') + : end != null && start.getTime() !== end.getTime() + ? i18n.t('equipmentScreen.bookingPeriod', { + begin: getRelativeDateString(start), + end: getRelativeDateString(end) + }) + : i18n.t('equipmentScreen.bookingDay', { + date: getRelativeDateString(start) + }) + } + - {this.state.timePickerVisible - ? - : null} { hideArrows={false} // Date marking style [simple/period/multi-dot/custom]. Default = 'simple' markingType={'period'} - markedDates={this.state.markedDates} + markedDates={{...this.lockedDates, ...this.state.markedDates}} theme={{ backgroundColor: this.props.theme.colors.agendaBackgroundColor, diff --git a/src/utils/EquipmentBooking.js b/src/utils/EquipmentBooking.js new file mode 100644 index 0000000..4537607 --- /dev/null +++ b/src/utils/EquipmentBooking.js @@ -0,0 +1,176 @@ +// @flow + +import type {Device} from "../screens/Amicale/Equipment/EquipmentListScreen"; +import i18n from "i18n-js"; +import DateManager from "../managers/DateManager"; +import type {CustomTheme} from "../managers/ThemeManager"; + +/** + * Gets the current day at midnight + * + * @returns {Date} + */ +export function getCurrentDay() { + let today = new Date(Date.now()); + today.setUTCHours(0, 0, 0, 0); + return today; +} + +/** + * Returns the ISO date format (without the time) + * + * @param date The date to recover the ISO format from + * @returns {*} + */ +export function getISODate(date: Date) { + return date.toISOString().split("T")[0]; +} + +/** + * Finds if the given equipment is available today + * + * @param item + * @returns {boolean} + */ +export function isEquipmentAvailable(item: Device) { + let isAvailable = true; + const today = getCurrentDay(); + const dates = item.booked_at; + for (let i = 0; i < dates.length; i++) { + const start = new Date(dates[i].begin); + const end = new Date(dates[i].end); + isAvailable = today < start || today > end; + if (!isAvailable) + break; + } + return isAvailable; +} + +/** + * Finds the first date free for booking. + * + * @param item + * @returns {Date} + */ +export function getFirstEquipmentAvailability(item: Device) { + let firstAvailability = getCurrentDay(); + const dates = item.booked_at; + for (let i = 0; i < dates.length; i++) { + const start = new Date(dates[i].begin); + let end = new Date(dates[i].end); + end.setDate(end.getDate() + 1); + if (firstAvailability >= start) + firstAvailability = end; + } + return firstAvailability; +} + +/** + * Gets a translated string representing the given date, relative to the current date + * + * @param date The date to translate + */ +export function getRelativeDateString(date: Date) { + const today = getCurrentDay(); + const yearDelta = date.getUTCFullYear() - today.getUTCFullYear(); + const monthDelta = date.getUTCMonth() - today.getUTCMonth(); + const dayDelta = date.getUTCDate() - today.getUTCDate(); + let translatedString = i18n.t('equipmentScreen.today'); + if (yearDelta > 0) + translatedString = i18n.t('equipmentScreen.otherYear', { + date: date.getDate(), + month: DateManager.getInstance().getMonthsOfYear()[date.getMonth()], + year: date.getFullYear() + }); + else if (monthDelta > 0) + translatedString = i18n.t('equipmentScreen.otherMonth', { + date: date.getDate(), + month: DateManager.getInstance().getMonthsOfYear()[date.getMonth()], + }); + else if (dayDelta > 1) + translatedString = i18n.t('equipmentScreen.thisMonth', { + date: date.getDate(), + }); + else if (dayDelta === 1) + translatedString = i18n.t('equipmentScreen.tomorrow'); + + return translatedString; +} + +/** + * Gets a valid array of dates between the given start and end, for the corresponding item. + * I stops at the first booked date encountered before the end. + * It assumes the range start and end are valid. + * + * Start and End specify the range's direction. + * If start < end, it will begin at Start and stop if it encounters any booked date before reaching End. + * If start > end, it will begin at End and stop if it encounters any booked dates before reaching Start. + * + * @param start Range start + * @param end Range end + * @param item Item containing booked dates to look for + * @returns {[string]} + */ +export function getValidRange(start: Date, end: Date, item: Device | null) { + let direction = start <= end ? 1 : -1; + let limit = new Date(end); + limit.setDate(limit.getDate() + direction); // Limit is excluded, but we want to include range end + if (item != null) { + if (direction === 1) { + for (let i = 0; i < item.booked_at.length; i++) { + const bookLimit = new Date(item.booked_at[i].begin); + if (start < bookLimit && limit > bookLimit) { + limit = bookLimit; + break; + } + } + } else { + for (let i = item.booked_at.length - 1; i >= 0; i--) { + const bookLimit = new Date(item.booked_at[i].end); + if (start > bookLimit && limit < bookLimit) { + limit = bookLimit; + break; + } + } + } + } + + + let validRange = []; + let date = new Date(start); + while ((direction === 1 && date < limit) || (direction === -1 && date > limit)) { + if (direction === 1) + validRange.push(getISODate(date)); + else + validRange.unshift(getISODate(date)); + date.setDate(date.getDate() + direction); + } + return validRange; +} + +/** + * Generates calendar compatible marked dates from the given array + * + * + * @param isSelection True to use user selection color, false to use disabled color + * @param theme The current App theme to get colors from + * @param range The range to mark dates for + * @returns {{}} + */ +export function generateMarkedDates(isSelection: boolean, theme: CustomTheme, range: Array) { + let markedDates = {} + for (let i = 0; i < range.length; i++) { + const isStart = i === 0; + const isEnd = i === range.length - 1; + markedDates[range[i]] = { + startingDay: isStart, + endingDay: isEnd, + color: isSelection + ? isStart || isEnd + ? theme.colors.primary + : theme.colors.danger + : theme.colors.textDisabled + }; + } + return markedDates; +} diff --git a/src/utils/Planning.js b/src/utils/Planning.js index ee1fd19..c4f59e3 100644 --- a/src/utils/Planning.js +++ b/src/utils/Planning.js @@ -114,7 +114,7 @@ export function stringToDate(dateString: string): Date | null { /** * Converts a date object to a string in the format - * YYYY-MM-DD HH-MM-SS + * YYYY-MM-DD HH-MM * * @param date The date object to convert * @param isUTC Whether to treat the date as UTC diff --git a/translations/en.json b/translations/en.json index ea969e1..c89bac0 100644 --- a/translations/en.json +++ b/translations/en.json @@ -452,12 +452,15 @@ "title": "Equipment booking", "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.", "bail": "Bail: %{cost}€", - "availableAt": "Available at: %{date}", - "available": "Available!", + "available": "Available %{date}", + "today": "today", + "tomorrow": "tomorrow", + "thisMonth": "the %{date}", + "otherMonth": "the %{date} of %{month}", + "otherYear": "the %{date} of %{month} %{year}", + "bookingDay": "Booked for %{date}", + "bookingPeriod": "Booked from %{begin} to %{end}", "booking": "Click on the calendar to set the start and end dates", - "startDate": "Start: ", - "endDate": "End: ", - "notSet": "Not set", "bookButton": "Book selected dates", "dialogTitle": "Confirm booking?", "dialogTitleLoading": "Sending your booking...",