Application Android et IOS pour l'amicale des élèves
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

EquipmentRentScreen.js 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  1. // @flow
  2. import * as React from 'react';
  3. import {
  4. Button,
  5. Caption,
  6. Card,
  7. Headline,
  8. Subheading,
  9. withTheme,
  10. } from 'react-native-paper';
  11. import {StackNavigationProp} from '@react-navigation/stack';
  12. import {BackHandler, View} from 'react-native';
  13. import * as Animatable from 'react-native-animatable';
  14. import i18n from 'i18n-js';
  15. import {CalendarList} from 'react-native-calendars';
  16. import type {DeviceType} from './EquipmentListScreen';
  17. import type {CustomTheme} from '../../../managers/ThemeManager';
  18. import LoadingConfirmDialog from '../../../components/Dialogs/LoadingConfirmDialog';
  19. import ErrorDialog from '../../../components/Dialogs/ErrorDialog';
  20. import {
  21. generateMarkedDates,
  22. getFirstEquipmentAvailability,
  23. getISODate,
  24. getRelativeDateString,
  25. getValidRange,
  26. isEquipmentAvailable,
  27. } from '../../../utils/EquipmentBooking';
  28. import ConnectionManager from '../../../managers/ConnectionManager';
  29. import CollapsibleScrollView from '../../../components/Collapsible/CollapsibleScrollView';
  30. type PropsType = {
  31. navigation: StackNavigationProp,
  32. route: {
  33. params?: {
  34. item?: DeviceType,
  35. },
  36. },
  37. theme: CustomTheme,
  38. };
  39. export type MarkedDatesObjectType = {
  40. [key: string]: {startingDay: boolean, endingDay: boolean, color: string},
  41. };
  42. type StateType = {
  43. dialogVisible: boolean,
  44. errorDialogVisible: boolean,
  45. markedDates: MarkedDatesObjectType,
  46. currentError: number,
  47. };
  48. class EquipmentRentScreen extends React.Component<PropsType, StateType> {
  49. item: DeviceType | null;
  50. bookedDates: Array<string>;
  51. bookRef: {current: null | Animatable.View};
  52. canBookEquipment: boolean;
  53. lockedDates: {
  54. [key: string]: {startingDay: boolean, endingDay: boolean, color: string},
  55. };
  56. constructor(props: PropsType) {
  57. super(props);
  58. this.state = {
  59. dialogVisible: false,
  60. errorDialogVisible: false,
  61. markedDates: {},
  62. currentError: 0,
  63. };
  64. this.resetSelection();
  65. this.bookRef = React.createRef();
  66. this.canBookEquipment = false;
  67. this.bookedDates = [];
  68. if (props.route.params != null) {
  69. if (props.route.params.item != null) this.item = props.route.params.item;
  70. else this.item = null;
  71. }
  72. const {item} = this;
  73. if (item != null) {
  74. this.lockedDates = {};
  75. item.booked_at.forEach((date: {begin: string, end: string}) => {
  76. const range = getValidRange(
  77. new Date(date.begin),
  78. new Date(date.end),
  79. null,
  80. );
  81. this.lockedDates = {
  82. ...this.lockedDates,
  83. ...generateMarkedDates(false, props.theme, range),
  84. };
  85. });
  86. }
  87. }
  88. /**
  89. * Captures focus and blur events to hook on android back button
  90. */
  91. componentDidMount() {
  92. const {navigation} = this.props;
  93. navigation.addListener('focus', () => {
  94. BackHandler.addEventListener(
  95. 'hardwareBackPress',
  96. this.onBackButtonPressAndroid,
  97. );
  98. });
  99. navigation.addListener('blur', () => {
  100. BackHandler.removeEventListener(
  101. 'hardwareBackPress',
  102. this.onBackButtonPressAndroid,
  103. );
  104. });
  105. }
  106. /**
  107. * Overrides default android back button behaviour to deselect date if any is selected.
  108. *
  109. * @return {boolean}
  110. */
  111. onBackButtonPressAndroid = (): boolean => {
  112. if (this.bookedDates.length > 0) {
  113. this.resetSelection();
  114. this.updateMarkedSelection();
  115. return true;
  116. }
  117. return false;
  118. };
  119. onDialogDismiss = () => {
  120. this.setState({dialogVisible: false});
  121. };
  122. onErrorDialogDismiss = () => {
  123. this.setState({errorDialogVisible: false});
  124. };
  125. /**
  126. * Sends the selected data to the server and waits for a response.
  127. * If the request is a success, navigate to the recap screen.
  128. * If it is an error, display the error to the user.
  129. *
  130. * @returns {Promise<void>}
  131. */
  132. onDialogAccept = (): Promise<void> => {
  133. return new Promise((resolve: () => void) => {
  134. const {item, props} = this;
  135. const start = this.getBookStartDate();
  136. const end = this.getBookEndDate();
  137. if (item != null && start != null && end != null) {
  138. ConnectionManager.getInstance()
  139. .authenticatedRequest('location/booking', {
  140. device: item.id,
  141. begin: getISODate(start),
  142. end: getISODate(end),
  143. })
  144. .then(() => {
  145. this.onDialogDismiss();
  146. props.navigation.replace('equipment-confirm', {
  147. item: this.item,
  148. dates: [getISODate(start), getISODate(end)],
  149. });
  150. resolve();
  151. })
  152. .catch((error: number) => {
  153. this.onDialogDismiss();
  154. this.showErrorDialog(error);
  155. resolve();
  156. });
  157. } else {
  158. this.onDialogDismiss();
  159. resolve();
  160. }
  161. });
  162. };
  163. getBookStartDate(): Date | null {
  164. return this.bookedDates.length > 0 ? new Date(this.bookedDates[0]) : null;
  165. }
  166. getBookEndDate(): Date | null {
  167. const {length} = this.bookedDates;
  168. return length > 0 ? new Date(this.bookedDates[length - 1]) : null;
  169. }
  170. /**
  171. * Selects a new date on the calendar.
  172. * If both start and end dates are already selected, unselect all.
  173. *
  174. * @param day The day selected
  175. */
  176. selectNewDate = (day: {
  177. dateString: string,
  178. day: number,
  179. month: number,
  180. timestamp: number,
  181. year: number,
  182. }) => {
  183. const selected = new Date(day.dateString);
  184. const start = this.getBookStartDate();
  185. if (!this.lockedDates[day.dateString] != null) {
  186. if (start === null) {
  187. this.updateSelectionRange(selected, selected);
  188. this.enableBooking();
  189. } else if (start.getTime() === selected.getTime()) {
  190. this.resetSelection();
  191. } else if (this.bookedDates.length === 1) {
  192. this.updateSelectionRange(start, selected);
  193. this.enableBooking();
  194. } else this.resetSelection();
  195. this.updateMarkedSelection();
  196. }
  197. };
  198. showErrorDialog = (error: number) => {
  199. this.setState({
  200. errorDialogVisible: true,
  201. currentError: error,
  202. });
  203. };
  204. showDialog = () => {
  205. this.setState({dialogVisible: true});
  206. };
  207. /**
  208. * Shows the book button by plying a fade animation
  209. */
  210. showBookButton() {
  211. if (this.bookRef.current != null) {
  212. this.bookRef.current.fadeInUp(500);
  213. }
  214. }
  215. /**
  216. * Hides the book button by plying a fade animation
  217. */
  218. hideBookButton() {
  219. if (this.bookRef.current != null) {
  220. this.bookRef.current.fadeOutDown(500);
  221. }
  222. }
  223. enableBooking() {
  224. if (!this.canBookEquipment) {
  225. this.showBookButton();
  226. this.canBookEquipment = true;
  227. }
  228. }
  229. resetSelection() {
  230. if (this.canBookEquipment) this.hideBookButton();
  231. this.canBookEquipment = false;
  232. this.bookedDates = [];
  233. }
  234. updateSelectionRange(start: Date, end: Date) {
  235. this.bookedDates = getValidRange(start, end, this.item);
  236. }
  237. updateMarkedSelection() {
  238. const {theme} = this.props;
  239. this.setState({
  240. markedDates: generateMarkedDates(true, theme, this.bookedDates),
  241. });
  242. }
  243. render(): React.Node {
  244. const {item, props, state} = this;
  245. const start = this.getBookStartDate();
  246. const end = this.getBookEndDate();
  247. let subHeadingText;
  248. if (start == null) subHeadingText = i18n.t('screens.equipment.booking');
  249. else if (end != null && start.getTime() !== end.getTime())
  250. subHeadingText = i18n.t('screens.equipment.bookingPeriod', {
  251. begin: getRelativeDateString(start),
  252. end: getRelativeDateString(end),
  253. });
  254. else
  255. i18n.t('screens.equipment.bookingDay', {
  256. date: getRelativeDateString(start),
  257. });
  258. if (item != null) {
  259. const isAvailable = isEquipmentAvailable(item);
  260. const firstAvailability = getFirstEquipmentAvailability(item);
  261. return (
  262. <View style={{flex: 1}}>
  263. <CollapsibleScrollView>
  264. <Card style={{margin: 5}}>
  265. <Card.Content>
  266. <View style={{flex: 1}}>
  267. <View
  268. style={{
  269. marginLeft: 'auto',
  270. marginRight: 'auto',
  271. flexDirection: 'row',
  272. flexWrap: 'wrap',
  273. }}>
  274. <Headline style={{textAlign: 'center'}}>
  275. {item.name}
  276. </Headline>
  277. <Caption
  278. style={{
  279. textAlign: 'center',
  280. lineHeight: 35,
  281. marginLeft: 10,
  282. }}>
  283. ({i18n.t('screens.equipment.bail', {cost: item.caution})})
  284. </Caption>
  285. </View>
  286. </View>
  287. <Button
  288. icon={isAvailable ? 'check-circle-outline' : 'update'}
  289. color={
  290. isAvailable
  291. ? props.theme.colors.success
  292. : props.theme.colors.primary
  293. }
  294. mode="text">
  295. {i18n.t('screens.equipment.available', {
  296. date: getRelativeDateString(firstAvailability),
  297. })}
  298. </Button>
  299. <Subheading
  300. style={{
  301. textAlign: 'center',
  302. marginBottom: 10,
  303. minHeight: 50,
  304. }}>
  305. {subHeadingText}
  306. </Subheading>
  307. </Card.Content>
  308. </Card>
  309. <CalendarList
  310. // Minimum date that can be selected, dates before minDate will be grayed out. Default = undefined
  311. minDate={new Date()}
  312. // Max amount of months allowed to scroll to the past. Default = 50
  313. pastScrollRange={0}
  314. // Max amount of months allowed to scroll to the future. Default = 50
  315. futureScrollRange={3}
  316. // Enable horizontal scrolling, default = false
  317. horizontal
  318. // Enable paging on horizontal, default = false
  319. pagingEnabled
  320. // Handler which gets executed on day press. Default = undefined
  321. onDayPress={this.selectNewDate}
  322. // If firstDay=1 week starts from Monday. Note that dayNames and dayNamesShort should still start from Sunday.
  323. firstDay={1}
  324. // Disable all touch events for disabled days. can be override with disableTouchEvent in markedDates
  325. disableAllTouchEventsForDisabledDays
  326. // Hide month navigation arrows.
  327. hideArrows={false}
  328. // Date marking style [simple/period/multi-dot/custom]. Default = 'simple'
  329. markingType="period"
  330. markedDates={{...this.lockedDates, ...state.markedDates}}
  331. theme={{
  332. backgroundColor: props.theme.colors.agendaBackgroundColor,
  333. calendarBackground: props.theme.colors.background,
  334. textSectionTitleColor: props.theme.colors.agendaDayTextColor,
  335. selectedDayBackgroundColor: props.theme.colors.primary,
  336. selectedDayTextColor: '#ffffff',
  337. todayTextColor: props.theme.colors.text,
  338. dayTextColor: props.theme.colors.text,
  339. textDisabledColor: props.theme.colors.agendaDayTextColor,
  340. dotColor: props.theme.colors.primary,
  341. selectedDotColor: '#ffffff',
  342. arrowColor: props.theme.colors.primary,
  343. monthTextColor: props.theme.colors.text,
  344. indicatorColor: props.theme.colors.primary,
  345. textDayFontFamily: 'monospace',
  346. textMonthFontFamily: 'monospace',
  347. textDayHeaderFontFamily: 'monospace',
  348. textDayFontWeight: '300',
  349. textMonthFontWeight: 'bold',
  350. textDayHeaderFontWeight: '300',
  351. textDayFontSize: 16,
  352. textMonthFontSize: 16,
  353. textDayHeaderFontSize: 16,
  354. 'stylesheet.day.period': {
  355. base: {
  356. overflow: 'hidden',
  357. height: 34,
  358. width: 34,
  359. alignItems: 'center',
  360. },
  361. },
  362. }}
  363. style={{marginBottom: 50}}
  364. />
  365. </CollapsibleScrollView>
  366. <LoadingConfirmDialog
  367. visible={state.dialogVisible}
  368. onDismiss={this.onDialogDismiss}
  369. onAccept={this.onDialogAccept}
  370. title={i18n.t('screens.equipment.dialogTitle')}
  371. titleLoading={i18n.t('screens.equipment.dialogTitleLoading')}
  372. message={i18n.t('screens.equipment.dialogMessage')}
  373. />
  374. <ErrorDialog
  375. visible={state.errorDialogVisible}
  376. onDismiss={this.onErrorDialogDismiss}
  377. errorCode={state.currentError}
  378. />
  379. <Animatable.View
  380. ref={this.bookRef}
  381. style={{
  382. position: 'absolute',
  383. bottom: 0,
  384. left: 0,
  385. width: '100%',
  386. flex: 1,
  387. transform: [{translateY: 100}],
  388. }}>
  389. <Button
  390. icon="bookmark-check"
  391. mode="contained"
  392. onPress={this.showDialog}
  393. style={{
  394. width: '80%',
  395. flex: 1,
  396. marginLeft: 'auto',
  397. marginRight: 'auto',
  398. marginBottom: 20,
  399. borderRadius: 10,
  400. }}>
  401. {i18n.t('screens.equipment.bookButton')}
  402. </Button>
  403. </Animatable.View>
  404. </View>
  405. );
  406. }
  407. return null;
  408. }
  409. }
  410. export default withTheme(EquipmentRentScreen);