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.

ProfileScreen.js 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494
  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. // @flow
  20. import * as React from 'react';
  21. import {FlatList, StyleSheet, View} from 'react-native';
  22. import {
  23. Avatar,
  24. Button,
  25. Card,
  26. Divider,
  27. List,
  28. Paragraph,
  29. withTheme,
  30. } from 'react-native-paper';
  31. import i18n from 'i18n-js';
  32. import {StackNavigationProp} from '@react-navigation/stack';
  33. import AuthenticatedScreen from '../../components/Amicale/AuthenticatedScreen';
  34. import LogoutDialog from '../../components/Amicale/LogoutDialog';
  35. import MaterialHeaderButtons, {
  36. Item,
  37. } from '../../components/Overrides/CustomHeaderButton';
  38. import CardList from '../../components/Lists/CardList/CardList';
  39. import type {CustomThemeType} from '../../managers/ThemeManager';
  40. import AvailableWebsites from '../../constants/AvailableWebsites';
  41. import Mascot, {MASCOT_STYLE} from '../../components/Mascot/Mascot';
  42. import ServicesManager, {SERVICES_KEY} from '../../managers/ServicesManager';
  43. import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList';
  44. import type {ServiceItemType} from '../../managers/ServicesManager';
  45. import type {
  46. CardTitleIconPropsType,
  47. ListIconPropsType,
  48. } from '../../constants/PaperStyles';
  49. type PropsType = {
  50. navigation: StackNavigationProp,
  51. theme: CustomThemeType,
  52. };
  53. type StateType = {
  54. dialogVisible: boolean,
  55. };
  56. type ClubType = {
  57. id: number,
  58. name: string,
  59. is_manager: boolean,
  60. };
  61. type ProfileDataType = {
  62. first_name: string,
  63. last_name: string,
  64. email: string,
  65. birthday: string,
  66. phone: string,
  67. branch: string,
  68. link: string,
  69. validity: boolean,
  70. clubs: Array<ClubType>,
  71. };
  72. const styles = StyleSheet.create({
  73. card: {
  74. margin: 10,
  75. },
  76. icon: {
  77. backgroundColor: 'transparent',
  78. },
  79. editButton: {
  80. marginLeft: 'auto',
  81. },
  82. });
  83. class ProfileScreen extends React.Component<PropsType, StateType> {
  84. data: ProfileDataType;
  85. flatListData: Array<{id: string}>;
  86. amicaleDataset: Array<ServiceItemType>;
  87. constructor(props: PropsType) {
  88. super(props);
  89. this.flatListData = [{id: '0'}, {id: '1'}, {id: '2'}, {id: '3'}];
  90. const services = new ServicesManager(props.navigation);
  91. this.amicaleDataset = services.getAmicaleServices([SERVICES_KEY.PROFILE]);
  92. this.state = {
  93. dialogVisible: false,
  94. };
  95. }
  96. componentDidMount() {
  97. const {navigation} = this.props;
  98. navigation.setOptions({
  99. headerRight: this.getHeaderButton,
  100. });
  101. }
  102. /**
  103. * Gets the logout header button
  104. *
  105. * @returns {*}
  106. */
  107. getHeaderButton = (): React.Node => (
  108. <MaterialHeaderButtons>
  109. <Item
  110. title="logout"
  111. iconName="logout"
  112. onPress={this.showDisconnectDialog}
  113. />
  114. </MaterialHeaderButtons>
  115. );
  116. /**
  117. * Gets the main screen component with the fetched data
  118. *
  119. * @param data The data fetched from the server
  120. * @returns {*}
  121. */
  122. getScreen = (data: Array<ProfileDataType | null>): React.Node => {
  123. const {dialogVisible} = this.state;
  124. const {navigation} = this.props;
  125. // eslint-disable-next-line prefer-destructuring
  126. if (data[0] != null) this.data = data[0];
  127. return (
  128. <View style={{flex: 1}}>
  129. <CollapsibleFlatList
  130. renderItem={this.getRenderItem}
  131. data={this.flatListData}
  132. />
  133. <LogoutDialog
  134. navigation={navigation}
  135. visible={dialogVisible}
  136. onDismiss={this.hideDisconnectDialog}
  137. />
  138. </View>
  139. );
  140. };
  141. getRenderItem = ({item}: {item: {id: string}}): React.Node => {
  142. switch (item.id) {
  143. case '0':
  144. return this.getWelcomeCard();
  145. case '1':
  146. return this.getPersonalCard();
  147. case '2':
  148. return this.getClubCard();
  149. default:
  150. return this.getMembershipCar();
  151. }
  152. };
  153. /**
  154. * Gets the list of services available with the Amicale account
  155. *
  156. * @returns {*}
  157. */
  158. getServicesList(): React.Node {
  159. return <CardList dataset={this.amicaleDataset} isHorizontal />;
  160. }
  161. /**
  162. * Gets a card welcoming the user to his account
  163. *
  164. * @returns {*}
  165. */
  166. getWelcomeCard(): React.Node {
  167. const {navigation} = this.props;
  168. return (
  169. <Card style={styles.card}>
  170. <Card.Title
  171. title={i18n.t('screens.profile.welcomeTitle', {
  172. name: this.data.first_name,
  173. })}
  174. left={(): React.Node => (
  175. <Mascot
  176. style={{
  177. width: 60,
  178. }}
  179. emotion={MASCOT_STYLE.COOL}
  180. animated
  181. entryAnimation={{
  182. animation: 'bounceIn',
  183. duration: 1000,
  184. }}
  185. />
  186. )}
  187. titleStyle={{marginLeft: 10}}
  188. />
  189. <Card.Content>
  190. <Divider />
  191. <Paragraph>{i18n.t('screens.profile.welcomeDescription')}</Paragraph>
  192. {this.getServicesList()}
  193. <Paragraph>{i18n.t('screens.profile.welcomeFeedback')}</Paragraph>
  194. <Divider />
  195. <Card.Actions>
  196. <Button
  197. icon="bug"
  198. mode="contained"
  199. onPress={() => {
  200. navigation.navigate('feedback');
  201. }}
  202. style={styles.editButton}>
  203. {i18n.t('screens.feedback.homeButtonTitle')}
  204. </Button>
  205. </Card.Actions>
  206. </Card.Content>
  207. </Card>
  208. );
  209. }
  210. /**
  211. * Gets the given field value.
  212. * If the field does not have a value, returns a placeholder text
  213. *
  214. * @param field The field to get the value from
  215. * @return {*}
  216. */
  217. static getFieldValue(field: ?string): string {
  218. return field != null ? field : i18n.t('screens.profile.noData');
  219. }
  220. /**
  221. * Gets a list item showing personal information
  222. *
  223. * @param field The field to display
  224. * @param icon The icon to use
  225. * @return {*}
  226. */
  227. getPersonalListItem(field: ?string, icon: string): React.Node {
  228. const {theme} = this.props;
  229. const title = field != null ? ProfileScreen.getFieldValue(field) : ':(';
  230. const subtitle = field != null ? '' : ProfileScreen.getFieldValue(field);
  231. return (
  232. <List.Item
  233. title={title}
  234. description={subtitle}
  235. left={(props: ListIconPropsType): React.Node => (
  236. <List.Icon
  237. style={props.style}
  238. icon={icon}
  239. color={field != null ? props.color : theme.colors.textDisabled}
  240. />
  241. )}
  242. />
  243. );
  244. }
  245. /**
  246. * Gets a card containing user personal information
  247. *
  248. * @return {*}
  249. */
  250. getPersonalCard(): React.Node {
  251. const {theme, navigation} = this.props;
  252. return (
  253. <Card style={styles.card}>
  254. <Card.Title
  255. title={`${this.data.first_name} ${this.data.last_name}`}
  256. subtitle={this.data.email}
  257. left={(iconProps: CardTitleIconPropsType): React.Node => (
  258. <Avatar.Icon
  259. size={iconProps.size}
  260. icon="account"
  261. color={theme.colors.primary}
  262. style={styles.icon}
  263. />
  264. )}
  265. />
  266. <Card.Content>
  267. <Divider />
  268. <List.Section>
  269. <List.Subheader>
  270. {i18n.t('screens.profile.personalInformation')}
  271. </List.Subheader>
  272. {this.getPersonalListItem(this.data.birthday, 'cake-variant')}
  273. {this.getPersonalListItem(this.data.phone, 'phone')}
  274. {this.getPersonalListItem(this.data.email, 'email')}
  275. {this.getPersonalListItem(this.data.branch, 'school')}
  276. </List.Section>
  277. <Divider />
  278. <Card.Actions>
  279. <Button
  280. icon="account-edit"
  281. mode="contained"
  282. onPress={() => {
  283. navigation.navigate('website', {
  284. host: AvailableWebsites.websites.AMICALE,
  285. path: this.data.link,
  286. title: i18n.t('screens.websites.amicale'),
  287. });
  288. }}
  289. style={styles.editButton}>
  290. {i18n.t('screens.profile.editInformation')}
  291. </Button>
  292. </Card.Actions>
  293. </Card.Content>
  294. </Card>
  295. );
  296. }
  297. /**
  298. * Gets a cars containing clubs the user is part of
  299. *
  300. * @return {*}
  301. */
  302. getClubCard(): React.Node {
  303. const {theme} = this.props;
  304. return (
  305. <Card style={styles.card}>
  306. <Card.Title
  307. title={i18n.t('screens.profile.clubs')}
  308. subtitle={i18n.t('screens.profile.clubsSubtitle')}
  309. left={(iconProps: CardTitleIconPropsType): React.Node => (
  310. <Avatar.Icon
  311. size={iconProps.size}
  312. icon="account-group"
  313. color={theme.colors.primary}
  314. style={styles.icon}
  315. />
  316. )}
  317. />
  318. <Card.Content>
  319. <Divider />
  320. {this.getClubList(this.data.clubs)}
  321. </Card.Content>
  322. </Card>
  323. );
  324. }
  325. /**
  326. * Gets a card showing if the user has payed his membership
  327. *
  328. * @return {*}
  329. */
  330. getMembershipCar(): React.Node {
  331. const {theme} = this.props;
  332. return (
  333. <Card style={styles.card}>
  334. <Card.Title
  335. title={i18n.t('screens.profile.membership')}
  336. subtitle={i18n.t('screens.profile.membershipSubtitle')}
  337. left={(iconProps: CardTitleIconPropsType): React.Node => (
  338. <Avatar.Icon
  339. size={iconProps.size}
  340. icon="credit-card"
  341. color={theme.colors.primary}
  342. style={styles.icon}
  343. />
  344. )}
  345. />
  346. <Card.Content>
  347. <List.Section>
  348. {this.getMembershipItem(this.data.validity)}
  349. </List.Section>
  350. </Card.Content>
  351. </Card>
  352. );
  353. }
  354. /**
  355. * Gets the item showing if the user has payed his membership
  356. *
  357. * @return {*}
  358. */
  359. getMembershipItem(state: boolean): React.Node {
  360. const {theme} = this.props;
  361. return (
  362. <List.Item
  363. title={
  364. state
  365. ? i18n.t('screens.profile.membershipPayed')
  366. : i18n.t('screens.profile.membershipNotPayed')
  367. }
  368. left={(props: ListIconPropsType): React.Node => (
  369. <List.Icon
  370. style={props.style}
  371. color={state ? theme.colors.success : theme.colors.danger}
  372. icon={state ? 'check' : 'close'}
  373. />
  374. )}
  375. />
  376. );
  377. }
  378. /**
  379. * Gets a list item for the club list
  380. *
  381. * @param item The club to render
  382. * @return {*}
  383. */
  384. getClubListItem = ({item}: {item: ClubType}): React.Node => {
  385. const {theme} = this.props;
  386. const onPress = () => {
  387. this.openClubDetailsScreen(item.id);
  388. };
  389. let description = i18n.t('screens.profile.isMember');
  390. let icon = (props: ListIconPropsType): React.Node => (
  391. <List.Icon color={props.color} style={props.style} icon="chevron-right" />
  392. );
  393. if (item.is_manager) {
  394. description = i18n.t('screens.profile.isManager');
  395. icon = (props: ListIconPropsType): React.Node => (
  396. <List.Icon
  397. style={props.style}
  398. icon="star"
  399. color={theme.colors.primary}
  400. />
  401. );
  402. }
  403. return (
  404. <List.Item
  405. title={item.name}
  406. description={description}
  407. left={icon}
  408. onPress={onPress}
  409. />
  410. );
  411. };
  412. /**
  413. * Renders the list of clubs the user is part of
  414. *
  415. * @param list The club list
  416. * @return {*}
  417. */
  418. getClubList(list: Array<ClubType>): React.Node {
  419. list.sort(this.sortClubList);
  420. return (
  421. <FlatList
  422. renderItem={this.getClubListItem}
  423. keyExtractor={this.clubKeyExtractor}
  424. data={list}
  425. />
  426. );
  427. }
  428. clubKeyExtractor = (item: ClubType): string => item.name;
  429. sortClubList = (a: ClubType): number => (a.is_manager ? -1 : 1);
  430. showDisconnectDialog = () => {
  431. this.setState({dialogVisible: true});
  432. };
  433. hideDisconnectDialog = () => {
  434. this.setState({dialogVisible: false});
  435. };
  436. /**
  437. * Opens the club details screen for the club of given ID
  438. * @param id The club's id to open
  439. */
  440. openClubDetailsScreen(id: number) {
  441. const {navigation} = this.props;
  442. navigation.navigate('club-information', {clubId: id});
  443. }
  444. render(): React.Node {
  445. const {navigation} = this.props;
  446. return (
  447. <AuthenticatedScreen
  448. navigation={navigation}
  449. requests={[
  450. {
  451. link: 'user/profile',
  452. params: {},
  453. mandatory: true,
  454. },
  455. ]}
  456. renderFunction={this.getScreen}
  457. />
  458. );
  459. }
  460. }
  461. export default withTheme(ProfileScreen);