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 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. // @flow
  2. import * as React from 'react';
  3. import {Button, Caption, Card, Headline, Subheading, withTheme} from 'react-native-paper';
  4. import {Collapsible} from "react-navigation-collapsible";
  5. import {withCollapsible} from "../../../utils/withCollapsible";
  6. import {StackNavigationProp} from "@react-navigation/stack";
  7. import type {CustomTheme} from "../../../managers/ThemeManager";
  8. import type {Device} from "./EquipmentListScreen";
  9. import {Animated, BackHandler, View} from "react-native";
  10. import * as Animatable from "react-native-animatable";
  11. import i18n from "i18n-js";
  12. import {CalendarList} from "react-native-calendars";
  13. import LoadingConfirmDialog from "../../../components/Dialogs/LoadingConfirmDialog";
  14. import ErrorDialog from "../../../components/Dialogs/ErrorDialog";
  15. import {
  16. generateMarkedDates,
  17. getFirstEquipmentAvailability,
  18. getISODate,
  19. getRelativeDateString,
  20. getValidRange,
  21. isEquipmentAvailable
  22. } from "../../../utils/EquipmentBooking";
  23. import ConnectionManager from "../../../managers/ConnectionManager";
  24. type Props = {
  25. navigation: StackNavigationProp,
  26. route: {
  27. params?: {
  28. item?: Device,
  29. },
  30. },
  31. theme: CustomTheme,
  32. collapsibleStack: Collapsible,
  33. }
  34. type State = {
  35. dialogVisible: boolean,
  36. errorDialogVisible: boolean,
  37. markedDates: { [key: string]: { startingDay: boolean, endingDay: boolean, color: string } },
  38. currentError: number,
  39. }
  40. class EquipmentRentScreen extends React.Component<Props, State> {
  41. state = {
  42. dialogVisible: false,
  43. errorDialogVisible: false,
  44. markedDates: {},
  45. currentError: 0,
  46. }
  47. item: Device | null;
  48. bookedDates: Array<string>;
  49. bookRef: { current: null | Animatable.View }
  50. canBookEquipment: boolean;
  51. lockedDates: { [key: string]: { startingDay: boolean, endingDay: boolean, color: string } }
  52. constructor(props: Props) {
  53. super(props);
  54. this.resetSelection();
  55. this.bookRef = React.createRef();
  56. this.canBookEquipment = false;
  57. this.bookedDates = [];
  58. if (this.props.route.params != null) {
  59. if (this.props.route.params.item != null)
  60. this.item = this.props.route.params.item;
  61. else
  62. this.item = null;
  63. }
  64. const item = this.item;
  65. if (item != null) {
  66. this.lockedDates = {};
  67. for (let i = 0; i < item.booked_at.length; i++) {
  68. const range = getValidRange(new Date(item.booked_at[i].begin), new Date(item.booked_at[i].end), null);
  69. this.lockedDates = {
  70. ...this.lockedDates,
  71. ...generateMarkedDates(
  72. false,
  73. this.props.theme,
  74. range
  75. )
  76. };
  77. }
  78. }
  79. }
  80. /**
  81. * Captures focus and blur events to hook on android back button
  82. */
  83. componentDidMount() {
  84. this.props.navigation.addListener(
  85. 'focus',
  86. () =>
  87. BackHandler.addEventListener(
  88. 'hardwareBackPress',
  89. this.onBackButtonPressAndroid
  90. )
  91. );
  92. this.props.navigation.addListener(
  93. 'blur',
  94. () =>
  95. BackHandler.removeEventListener(
  96. 'hardwareBackPress',
  97. this.onBackButtonPressAndroid
  98. )
  99. );
  100. }
  101. /**
  102. * Overrides default android back button behaviour to deselect date if any is selected.
  103. *
  104. * @return {boolean}
  105. */
  106. onBackButtonPressAndroid = () => {
  107. if (this.bookedDates.length > 0) {
  108. this.resetSelection();
  109. this.updateMarkedSelection();
  110. return true;
  111. } else
  112. return false;
  113. };
  114. /**
  115. * Selects a new date on the calendar.
  116. * If both start and end dates are already selected, unselect all.
  117. *
  118. * @param day The day selected
  119. */
  120. selectNewDate = (day: { dateString: string, day: number, month: number, timestamp: number, year: number }) => {
  121. const selected = new Date(day.dateString);
  122. const start = this.getBookStartDate();
  123. if (!(this.lockedDates.hasOwnProperty(day.dateString))) {
  124. if (start === null) {
  125. this.updateSelectionRange(selected, selected);
  126. this.enableBooking();
  127. } else if (start.getTime() === selected.getTime()) {
  128. this.resetSelection();
  129. } else if (this.bookedDates.length === 1) {
  130. this.updateSelectionRange(start, selected);
  131. this.enableBooking();
  132. } else
  133. this.resetSelection();
  134. this.updateMarkedSelection();
  135. }
  136. }
  137. updateSelectionRange(start: Date, end: Date) {
  138. this.bookedDates = getValidRange(start, end, this.item);
  139. }
  140. updateMarkedSelection() {
  141. this.setState({
  142. markedDates: generateMarkedDates(
  143. true,
  144. this.props.theme,
  145. this.bookedDates
  146. ),
  147. });
  148. }
  149. enableBooking() {
  150. if (!this.canBookEquipment) {
  151. this.showBookButton();
  152. this.canBookEquipment = true;
  153. }
  154. }
  155. resetSelection() {
  156. if (this.canBookEquipment)
  157. this.hideBookButton();
  158. this.canBookEquipment = false;
  159. this.bookedDates = [];
  160. }
  161. /**
  162. * Shows the book button by plying a fade animation
  163. */
  164. showBookButton() {
  165. if (this.bookRef.current != null) {
  166. this.bookRef.current.fadeInUp(500);
  167. }
  168. }
  169. /**
  170. * Hides the book button by plying a fade animation
  171. */
  172. hideBookButton() {
  173. if (this.bookRef.current != null) {
  174. this.bookRef.current.fadeOutDown(500);
  175. }
  176. }
  177. showDialog = () => {
  178. this.setState({dialogVisible: true});
  179. }
  180. showErrorDialog = (error: number) => {
  181. this.setState({
  182. errorDialogVisible: true,
  183. currentError: error,
  184. });
  185. }
  186. onDialogDismiss = () => {
  187. this.setState({dialogVisible: false});
  188. }
  189. onErrorDialogDismiss = () => {
  190. this.setState({errorDialogVisible: false});
  191. }
  192. /**
  193. * Sends the selected data to the server and waits for a response.
  194. * If the request is a success, navigate to the recap screen.
  195. * If it is an error, display the error to the user.
  196. *
  197. * @returns {Promise<R>}
  198. */
  199. onDialogAccept = () => {
  200. return new Promise((resolve) => {
  201. const item = this.item;
  202. const start = this.getBookStartDate();
  203. const end = this.getBookEndDate();
  204. if (item != null && start != null && end != null) {
  205. console.log({
  206. "device": item.id,
  207. "begin": getISODate(start),
  208. "end": getISODate(end),
  209. })
  210. ConnectionManager.getInstance().authenticatedRequest(
  211. "location/booking",
  212. {
  213. "device": item.id,
  214. "begin": getISODate(start),
  215. "end": getISODate(end),
  216. })
  217. .then(() => {
  218. this.onDialogDismiss();
  219. this.props.navigation.replace("equipment-confirm", {
  220. item: this.item,
  221. dates: [getISODate(start), getISODate(end)]
  222. });
  223. resolve();
  224. })
  225. .catch((error: number) => {
  226. this.onDialogDismiss();
  227. this.showErrorDialog(error);
  228. resolve();
  229. });
  230. } else {
  231. this.onDialogDismiss();
  232. resolve();
  233. }
  234. });
  235. }
  236. getBookStartDate() {
  237. return this.bookedDates.length > 0 ? new Date(this.bookedDates[0]) : null;
  238. }
  239. getBookEndDate() {
  240. const length = this.bookedDates.length;
  241. return length > 0 ? new Date(this.bookedDates[length - 1]) : null;
  242. }
  243. render() {
  244. const {containerPaddingTop, scrollIndicatorInsetTop, onScroll} = this.props.collapsibleStack;
  245. const item = this.item;
  246. const start = this.getBookStartDate();
  247. const end = this.getBookEndDate();
  248. if (item != null) {
  249. const isAvailable = isEquipmentAvailable(item);
  250. const firstAvailability = getFirstEquipmentAvailability(item);
  251. return (
  252. <View style={{flex: 1}}>
  253. <Animated.ScrollView
  254. // Animations
  255. onScroll={onScroll}
  256. contentContainerStyle={{
  257. paddingTop: containerPaddingTop,
  258. minHeight: '100%'
  259. }}
  260. scrollIndicatorInsets={{top: scrollIndicatorInsetTop}}>
  261. <Card style={{margin: 5}}>
  262. <Card.Content>
  263. <View style={{flex: 1}}>
  264. <View style={{
  265. marginLeft: "auto",
  266. marginRight: "auto",
  267. flexDirection: "row",
  268. flexWrap: "wrap",
  269. }}>
  270. <Headline style={{textAlign: "center"}}>
  271. {item.name}
  272. </Headline>
  273. <Caption style={{
  274. textAlign: "center",
  275. lineHeight: 35,
  276. marginLeft: 10,
  277. }}>
  278. ({i18n.t('screens.equipment.bail', {cost: item.caution})})
  279. </Caption>
  280. </View>
  281. </View>
  282. <Button
  283. icon={isAvailable ? "check-circle-outline" : "update"}
  284. color={isAvailable ? this.props.theme.colors.success : this.props.theme.colors.primary}
  285. mode="text"
  286. >
  287. {i18n.t('screens.equipment.available', {date: getRelativeDateString(firstAvailability)})}
  288. </Button>
  289. <Subheading style={{
  290. textAlign: "center",
  291. marginBottom: 10,
  292. minHeight: 50
  293. }}>
  294. {
  295. start == null
  296. ? i18n.t('screens.equipment.booking')
  297. : end != null && start.getTime() !== end.getTime()
  298. ? i18n.t('screens.equipment.bookingPeriod', {
  299. begin: getRelativeDateString(start),
  300. end: getRelativeDateString(end)
  301. })
  302. : i18n.t('screens.equipment.bookingDay', {
  303. date: getRelativeDateString(start)
  304. })
  305. }
  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={true}
  318. // Enable paging on horizontal, default = false
  319. pagingEnabled={true}
  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={true}
  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, ...this.state.markedDates}}
  331. theme={{
  332. backgroundColor: this.props.theme.colors.agendaBackgroundColor,
  333. calendarBackground: this.props.theme.colors.background,
  334. textSectionTitleColor: this.props.theme.colors.agendaDayTextColor,
  335. selectedDayBackgroundColor: this.props.theme.colors.primary,
  336. selectedDayTextColor: '#ffffff',
  337. todayTextColor: this.props.theme.colors.text,
  338. dayTextColor: this.props.theme.colors.text,
  339. textDisabledColor: this.props.theme.colors.agendaDayTextColor,
  340. dotColor: this.props.theme.colors.primary,
  341. selectedDotColor: '#ffffff',
  342. arrowColor: this.props.theme.colors.primary,
  343. monthTextColor: this.props.theme.colors.text,
  344. indicatorColor: this.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. </Animated.ScrollView>
  366. <LoadingConfirmDialog
  367. visible={this.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={this.state.errorDialogVisible}
  376. onDismiss={this.onErrorDialogDismiss}
  377. errorCode={this.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: [
  388. {translateY: 100},
  389. ]
  390. }}>
  391. <Button
  392. icon="bookmark-check"
  393. mode="contained"
  394. onPress={this.showDialog}
  395. style={{
  396. width: "80%",
  397. flex: 1,
  398. marginLeft: "auto",
  399. marginRight: "auto",
  400. marginBottom: 20,
  401. borderRadius: 10
  402. }}
  403. >
  404. {i18n.t('screens.equipment.bookButton')}
  405. </Button>
  406. </Animatable.View>
  407. </View>
  408. )
  409. } else
  410. return <View/>;
  411. }
  412. }
  413. export default withCollapsible(withTheme(EquipmentRentScreen));