Application Android et IOS pour l'amicale des élèves https://play.google.com/store/apps/details?id=fr.amicaleinsat.application
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.tsx 15KB

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