Improved equipment rent screens to match new api version

This commit is contained in:
Arnaud Vergnet 2020-07-10 15:04:35 +02:00
parent 5067fd47d6
commit e048035722
9 changed files with 623 additions and 298 deletions

View file

@ -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);
});

View file

@ -21,7 +21,6 @@
"dependencies": { "dependencies": {
"@nartc/react-native-barcode-mask": "^1.2.0", "@nartc/react-native-barcode-mask": "^1.2.0",
"@react-native-community/async-storage": "^1.11.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/masked-view": "^0.1.10",
"@react-native-community/push-notification-ios": "^1.2.2", "@react-native-community/push-notification-ios": "^1.2.2",
"@react-native-community/slider": "^3.0.0", "@react-native-community/slider": "^3.0.0",

View file

@ -5,7 +5,11 @@ import {Avatar, List, withTheme} from 'react-native-paper';
import type {CustomTheme} from "../../../managers/ThemeManager"; import type {CustomTheme} from "../../../managers/ThemeManager";
import type {Device} from "../../../screens/Amicale/Equipment/EquipmentListScreen"; import type {Device} from "../../../screens/Amicale/Equipment/EquipmentListScreen";
import i18n from "i18n-js"; import i18n from "i18n-js";
import {getTimeOnlyString, stringToDate} from "../../../utils/Planning"; import {
getFirstEquipmentAvailability,
getRelativeDateString,
isEquipmentAvailable
} from "../../../utils/EquipmentBooking";
type Props = { type Props = {
onPress: () => void, onPress: () => void,
@ -20,41 +24,17 @@ class EquipmentListItem extends React.Component<Props> {
return false; 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() { render() {
const colors = this.props.theme.colors; const colors = this.props.theme.colors;
const item = this.props.item; const item = this.props.item;
const isAvailable = this.isAvailable(); const isAvailable = isEquipmentAvailable(item);
const firstAvailability = getFirstEquipmentAvailability(item);
return ( return (
<List.Item <List.Item
title={item.name} title={item.name}
description={isAvailable description={isAvailable
? i18n.t('equipmentScreen.bail', {cost: item.caution}) ? i18n.t('equipmentScreen.bail', {cost: item.caution})
: i18n.t('equipmentScreen.availableAt', {date: this.getDateString(item.available_at)})} : i18n.t('equipmentScreen.available', {date: getRelativeDateString(firstAvailability)})}
onPress={this.props.onPress} onPress={this.props.onPress}
left={(props) => <Avatar.Icon left={(props) => <Avatar.Icon
{...props} {...props}

View file

@ -49,6 +49,10 @@ export default class DateManager {
return date.getDay() === 6 || date.getDay() === 0; return date.getDay() === 6 || date.getDay() === 0;
} }
getMonthsOfYear() {
return this.monthsOfYear;
}
/** /**
* Gets a translated string representing the given date. * Gets a translated string representing the given date.
* *

View file

@ -22,36 +22,9 @@ export type Device = {
id: number, id: number,
name: string, name: string,
caution: number, caution: number,
available_at: string, booked_at: Array<{begin: string, end: string}>,
}; };
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 ICON_AMICALE = require('../../../../assets/amicale.png');
const LIST_ITEM_HEIGHT = 64; const LIST_ITEM_HEIGHT = 64;
@ -104,8 +77,6 @@ class EquipmentListScreen extends React.Component<Props> {
const fetchedData = data[0]; const fetchedData = data[0];
if (fetchedData != null) if (fetchedData != null)
this.data = fetchedData["devices"]; this.data = fetchedData["devices"];
this.data = TEST_DATASET; // TODO remove in prod
} }
const {containerPaddingTop, scrollIndicatorInsetTop, onScroll} = this.props.collapsibleStack; const {containerPaddingTop, scrollIndicatorInsetTop, onScroll} = this.props.collapsibleStack;
return ( return (
@ -131,9 +102,9 @@ class EquipmentListScreen extends React.Component<Props> {
{...this.props} {...this.props}
requests={[ requests={[
{ {
link: 'user/profile', link: 'location/all',
params: {}, params: {},
mandatory: false, mandatory: true,
} }
]} ]}
renderFunction={this.getScreen} renderFunction={this.getScreen}

View file

@ -1,7 +1,7 @@
// @flow // @flow
import * as React from 'react'; 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 {Collapsible} from "react-navigation-collapsible";
import {withCollapsible} from "../../../utils/withCollapsible"; import {withCollapsible} from "../../../utils/withCollapsible";
import {StackNavigationProp} from "@react-navigation/stack"; import {StackNavigationProp} from "@react-navigation/stack";
@ -11,12 +11,18 @@ import {Animated, BackHandler} from "react-native";
import * as Animatable from "react-native-animatable"; import * as Animatable from "react-native-animatable";
import {View} from "react-native-animatable"; import {View} from "react-native-animatable";
import i18n from "i18n-js"; import i18n from "i18n-js";
import {dateToString, getTimeOnlyString, stringToDate} from "../../../utils/Planning";
import {CalendarList} from "react-native-calendars"; import {CalendarList} from "react-native-calendars";
import DateTimePicker from '@react-native-community/datetimepicker';
import LoadingConfirmDialog from "../../../components/Dialogs/LoadingConfirmDialog"; import LoadingConfirmDialog from "../../../components/Dialogs/LoadingConfirmDialog";
import ConnectionManager from "../../../managers/ConnectionManager"; import ConnectionManager from "../../../managers/ConnectionManager";
import ErrorDialog from "../../../components/Dialogs/ErrorDialog"; import ErrorDialog from "../../../components/Dialogs/ErrorDialog";
import {
generateMarkedDates,
getFirstEquipmentAvailability,
getISODate,
getRelativeDateString,
getValidRange,
isEquipmentAvailable
} from "../../../utils/EquipmentBooking";
type Props = { type Props = {
navigation: StackNavigationProp, navigation: StackNavigationProp,
@ -33,7 +39,6 @@ type State = {
dialogVisible: boolean, dialogVisible: boolean,
errorDialogVisible: boolean, errorDialogVisible: boolean,
markedDates: { [key: string]: { startingDay: boolean, endingDay: boolean, color: string } }, markedDates: { [key: string]: { startingDay: boolean, endingDay: boolean, color: string } },
timePickerVisible: boolean,
currentError: number, currentError: number,
} }
@ -43,32 +48,45 @@ class EquipmentRentScreen extends React.Component<Props, State> {
dialogVisible: false, dialogVisible: false,
errorDialogVisible: false, errorDialogVisible: false,
markedDates: {}, markedDates: {},
timePickerVisible: false,
currentError: 0, currentError: 0,
} }
item: Device | null; item: Device | null;
selectedDates: { bookedDates: Array<string>;
start: Date | null,
end: Date | null,
};
currentlySelectedDate: Date | null;
bookRef: { current: null | Animatable.View } bookRef: { current: null | Animatable.View }
canBookEquipment: boolean; canBookEquipment: boolean;
lockedDates: { [key: string]: { startingDay: boolean, endingDay: boolean, color: string } }
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
this.resetSelection(); this.resetSelection();
this.bookRef = React.createRef(); this.bookRef = React.createRef();
this.canBookEquipment = false; this.canBookEquipment = false;
this.bookedDates = [];
if (this.props.route.params != null) { if (this.props.route.params != null) {
if (this.props.route.params.item != null) if (this.props.route.params.item != null)
this.item = this.props.route.params.item; this.item = this.props.route.params.item;
else else
this.item = null; 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<Props, State> {
* @return {boolean} * @return {boolean}
*/ */
onBackButtonPressAndroid = () => { onBackButtonPressAndroid = () => {
if (this.currentlySelectedDate != null) { if (this.bookedDates.length > 0) {
this.resetSelection(); this.resetSelection();
this.setState({ this.updateMarkedSelection();
markedDates: this.generateMarkedDates(),
});
return true; return true;
} else } else
return false; 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. * Selects a new date on the calendar.
* If both start and end dates are already selected, unselect all. * If both start and end dates are already selected, unselect all.
@ -157,29 +132,42 @@ class EquipmentRentScreen extends React.Component<Props, State> {
* @param day The day selected * @param day The day selected
*/ */
selectNewDate = (day: { dateString: string, day: number, month: number, timestamp: number, year: number }) => { 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) { if (!(this.lockedDates.hasOwnProperty(day.dateString))) {
const start = this.selectedDates.start; if (start === null) {
if (start == null) this.updateSelectionRange(selected, selected);
this.selectedDates.start = this.currentlySelectedDate; this.enableBooking();
else if (this.currentlySelectedDate < start) { } else if (start.getTime() === selected.getTime()) {
this.selectedDates.end = start; this.resetSelection();
this.selectedDates.start = this.currentlySelectedDate; } else if (this.bookedDates.length === 1) {
this.updateSelectionRange(start, selected);
this.enableBooking();
} else } else
this.selectedDates.end = this.currentlySelectedDate; this.resetSelection();
} else this.updateMarkedSelection();
this.resetSelection(); }
}
if (this.selectedDates.start != null) { updateSelectionRange(start: Date, end: Date) {
this.setState({ this.bookedDates = getValidRange(start, end, this.item);
markedDates: this.generateMarkedDates(), }
timePickerVisible: true,
}); updateMarkedSelection() {
} else { this.setState({
this.setState({ markedDates: generateMarkedDates(
markedDates: this.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<Props, State> {
if (this.canBookEquipment) if (this.canBookEquipment)
this.hideBookButton(); this.hideBookButton();
this.canBookEquipment = false; this.canBookEquipment = false;
this.selectedDates = {start: null, end: null}; this.bookedDates = [];
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;
} }
/** /**
@ -349,15 +225,15 @@ class EquipmentRentScreen extends React.Component<Props, State> {
onDialogAccept = () => { onDialogAccept = () => {
return new Promise((resolve) => { return new Promise((resolve) => {
const item = this.item; const item = this.item;
const start = this.selectedDates.start; const start = this.getBookStartDate();
const end = this.selectedDates.end; const end = this.getBookEndDate();
if (item != null && start != null && end != null) { if (item != null && start != null && end != null) {
ConnectionManager.getInstance().authenticatedRequest( ConnectionManager.getInstance().authenticatedRequest(
"", // TODO set path "location/booking",
{ {
"id": item.id, "device": item.id,
"start": dateToString(start, false), "begin": getISODate(start),
"end": dateToString(end, false), "end": getISODate(end),
}) })
.then(() => { .then(() => {
console.log("Success, replace screen"); console.log("Success, replace screen");
@ -373,20 +249,25 @@ class EquipmentRentScreen extends React.Component<Props, State> {
}); });
} }
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() { render() {
const {containerPaddingTop, scrollIndicatorInsetTop, onScroll} = this.props.collapsibleStack; const {containerPaddingTop, scrollIndicatorInsetTop, onScroll} = this.props.collapsibleStack;
let startString = <Caption>{i18n.t('equipmentScreen.notSet')}</Caption>;
let endString = <Caption>{i18n.t('equipmentScreen.notSet')}</Caption>;
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 item = this.item;
const start = this.getBookStartDate();
const end = this.getBookEndDate();
if (item != null) { if (item != null) {
const isAvailable = this.isAvailable(item); const isAvailable = isEquipmentAvailable(item);
const firstAvailability = getFirstEquipmentAvailability(item);
return ( return (
<View style={{flex: 1}}> <View style={{flex: 1}}>
<Animated.ScrollView <Animated.ScrollView
@ -424,40 +305,32 @@ class EquipmentRentScreen extends React.Component<Props, State> {
color={isAvailable ? this.props.theme.colors.success : this.props.theme.colors.primary} color={isAvailable ? this.props.theme.colors.success : this.props.theme.colors.primary}
mode="text" mode="text"
> >
{ {i18n.t('equipmentScreen.available', {date: getRelativeDateString(firstAvailability)})}
isAvailable
? i18n.t('equipmentScreen.available')
: i18n.t('equipmentScreen.availableAt', {date: this.getDateString(item.available_at)})
}
</Button> </Button>
<Text style={{ <Subheading style={{
textAlign: "center", textAlign: "center",
marginBottom: 10 marginBottom: 10,
minHeight: 50
}}> }}>
{i18n.t('equipmentScreen.booking')} {
</Text> start == null
<Subheading style={{textAlign: "center"}}> ? i18n.t('equipmentScreen.booking')
{i18n.t('equipmentScreen.startDate')} : end != null && start.getTime() !== end.getTime()
{startString} ? i18n.t('equipmentScreen.bookingPeriod', {
</Subheading> begin: getRelativeDateString(start),
<Subheading style={{textAlign: "center"}}> end: getRelativeDateString(end)
{i18n.t('equipmentScreen.endDate')} })
{endString} : i18n.t('equipmentScreen.bookingDay', {
date: getRelativeDateString(start)
})
}
</Subheading> </Subheading>
</Card.Content> </Card.Content>
</Card> </Card>
{this.state.timePickerVisible
? <DateTimePicker
value={new Date()}
mode={"time"}
display={"clock"}
is24Hour={true}
onChange={this.onTimeChange}
/>
: null}
<CalendarList <CalendarList
// Minimum date that can be selected, dates before minDate will be grayed out. Default = undefined // Minimum date that can be selected, dates before minDate will be grayed out. Default = undefined
minDate={this.getMinDate(item, isAvailable)} minDate={new Date()}
// Max amount of months allowed to scroll to the past. Default = 50 // Max amount of months allowed to scroll to the past. Default = 50
pastScrollRange={0} pastScrollRange={0}
// Max amount of months allowed to scroll to the future. Default = 50 // Max amount of months allowed to scroll to the future. Default = 50
@ -476,7 +349,7 @@ class EquipmentRentScreen extends React.Component<Props, State> {
hideArrows={false} hideArrows={false}
// Date marking style [simple/period/multi-dot/custom]. Default = 'simple' // Date marking style [simple/period/multi-dot/custom]. Default = 'simple'
markingType={'period'} markingType={'period'}
markedDates={this.state.markedDates} markedDates={{...this.lockedDates, ...this.state.markedDates}}
theme={{ theme={{
backgroundColor: this.props.theme.colors.agendaBackgroundColor, backgroundColor: this.props.theme.colors.agendaBackgroundColor,

View file

@ -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<string>) {
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;
}

View file

@ -114,7 +114,7 @@ export function stringToDate(dateString: string): Date | null {
/** /**
* Converts a date object to a string in the format * 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 date The date object to convert
* @param isUTC Whether to treat the date as UTC * @param isUTC Whether to treat the date as UTC

View file

@ -452,12 +452,15 @@
"title": "Equipment booking", "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.", "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}€", "bail": "Bail: %{cost}€",
"availableAt": "Available at: %{date}", "available": "Available %{date}",
"available": "Available!", "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", "booking": "Click on the calendar to set the start and end dates",
"startDate": "Start: ",
"endDate": "End: ",
"notSet": "Not set",
"bookButton": "Book selected dates", "bookButton": "Book selected dates",
"dialogTitle": "Confirm booking?", "dialogTitle": "Confirm booking?",
"dialogTitleLoading": "Sending your booking...", "dialogTitleLoading": "Sending your booking...",