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.

VoteScreen.tsx 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  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 {RefreshControl, View} from 'react-native';
  21. import {StackNavigationProp} from '@react-navigation/stack';
  22. import i18n from 'i18n-js';
  23. import {Button} from 'react-native-paper';
  24. import AuthenticatedScreen from '../../components/Amicale/AuthenticatedScreen';
  25. import {getTimeOnlyString, stringToDate} from '../../utils/Planning';
  26. import VoteTease from '../../components/Amicale/Vote/VoteTease';
  27. import VoteSelect from '../../components/Amicale/Vote/VoteSelect';
  28. import VoteResults from '../../components/Amicale/Vote/VoteResults';
  29. import VoteWait from '../../components/Amicale/Vote/VoteWait';
  30. import {MASCOT_STYLE} from '../../components/Mascot/Mascot';
  31. import MascotPopup from '../../components/Mascot/MascotPopup';
  32. import AsyncStorageManager from '../../managers/AsyncStorageManager';
  33. import VoteNotAvailable from '../../components/Amicale/Vote/VoteNotAvailable';
  34. import CollapsibleFlatList from '../../components/Collapsible/CollapsibleFlatList';
  35. export type VoteTeamType = {
  36. id: number;
  37. name: string;
  38. votes: number;
  39. };
  40. type TeamResponseType = {
  41. has_voted: boolean;
  42. teams: Array<VoteTeamType>;
  43. };
  44. type VoteDatesStringType = {
  45. date_begin: string;
  46. date_end: string;
  47. date_result_begin: string;
  48. date_result_end: string;
  49. };
  50. type VoteDatesObjectType = {
  51. date_begin: Date;
  52. date_end: Date;
  53. date_result_begin: Date;
  54. date_result_end: Date;
  55. };
  56. // const FAKE_DATE = {
  57. // "date_begin": "2020-08-19 15:50",
  58. // "date_end": "2020-08-19 15:50",
  59. // "date_result_begin": "2020-08-19 19:50",
  60. // "date_result_end": "2020-08-19 22:50",
  61. // };
  62. //
  63. // const FAKE_DATE2 = {
  64. // "date_begin": null,
  65. // "date_end": null,
  66. // "date_result_begin": null,
  67. // "date_result_end": null,
  68. // };
  69. //
  70. // const FAKE_TEAMS = {
  71. // has_voted: false,
  72. // teams: [
  73. // {
  74. // id: 1,
  75. // name: "TEST TEAM1",
  76. // },
  77. // {
  78. // id: 2,
  79. // name: "TEST TEAM2",
  80. // },
  81. // ],
  82. // };
  83. // const FAKE_TEAMS2 = {
  84. // has_voted: false,
  85. // teams: [
  86. // {
  87. // id: 1,
  88. // name: "TEST TEAM1",
  89. // votes: 9,
  90. // },
  91. // {
  92. // id: 2,
  93. // name: "TEST TEAM2",
  94. // votes: 9,
  95. // },
  96. // {
  97. // id: 3,
  98. // name: "TEST TEAM3",
  99. // votes: 5,
  100. // },
  101. // ],
  102. // };
  103. const MIN_REFRESH_TIME = 5 * 1000;
  104. type PropsType = {
  105. navigation: StackNavigationProp<any>;
  106. };
  107. type StateType = {
  108. hasVoted: boolean;
  109. mascotDialogVisible: boolean;
  110. };
  111. /**
  112. * Screen displaying vote information and controls
  113. */
  114. export default class VoteScreen extends React.Component<PropsType, StateType> {
  115. teams: Array<VoteTeamType>;
  116. hasVoted: boolean;
  117. datesString: null | VoteDatesStringType;
  118. dates: null | VoteDatesObjectType;
  119. today: Date;
  120. mainFlatListData: Array<{key: string}>;
  121. lastRefresh: Date | null;
  122. authRef: {current: null | AuthenticatedScreen<any>};
  123. constructor(props: PropsType) {
  124. super(props);
  125. this.teams = [];
  126. this.datesString = null;
  127. this.dates = null;
  128. this.state = {
  129. hasVoted: false,
  130. mascotDialogVisible: AsyncStorageManager.getBool(
  131. AsyncStorageManager.PREFERENCES.voteShowMascot.key,
  132. ),
  133. };
  134. this.hasVoted = false;
  135. this.today = new Date();
  136. this.authRef = React.createRef();
  137. this.lastRefresh = null;
  138. this.mainFlatListData = [{key: 'main'}, {key: 'info'}];
  139. }
  140. /**
  141. * Gets the string representation of the given date.
  142. *
  143. * If the given date is the same day as today, only return the tile.
  144. * Otherwise, return the full date.
  145. *
  146. * @param date The Date object representation of the wanted date
  147. * @param dateString The string representation of the wanted date
  148. * @returns {string}
  149. */
  150. getDateString(date: Date, dateString: string): string {
  151. if (this.today.getDate() === date.getDate()) {
  152. const str = getTimeOnlyString(dateString);
  153. return str != null ? str : '';
  154. }
  155. return dateString;
  156. }
  157. getMainRenderItem = ({item}: {item: {key: string}}) => {
  158. if (item.key === 'info') {
  159. return (
  160. <View>
  161. <Button
  162. mode="contained"
  163. icon="help-circle"
  164. onPress={this.showMascotDialog}
  165. style={{
  166. marginLeft: 'auto',
  167. marginRight: 'auto',
  168. marginTop: 20,
  169. }}>
  170. {i18n.t('screens.vote.mascotDialog.title')}
  171. </Button>
  172. </View>
  173. );
  174. }
  175. return this.getContent();
  176. };
  177. getScreen = (data: Array<TeamResponseType | VoteDatesStringType | null>) => {
  178. const {state} = this;
  179. // data[0] = FAKE_TEAMS2;
  180. // data[1] = FAKE_DATE;
  181. this.lastRefresh = new Date();
  182. const teams = data[0] as TeamResponseType | null;
  183. const dateStrings = data[1] as VoteDatesStringType | null;
  184. if (dateStrings != null && dateStrings.date_begin == null) {
  185. this.datesString = null;
  186. } else {
  187. this.datesString = dateStrings;
  188. }
  189. if (teams != null) {
  190. this.teams = teams.teams;
  191. this.hasVoted = teams.has_voted;
  192. }
  193. this.generateDateObject();
  194. return (
  195. <CollapsibleFlatList
  196. data={this.mainFlatListData}
  197. refreshControl={
  198. <RefreshControl refreshing={false} onRefresh={this.reloadData} />
  199. }
  200. extraData={state.hasVoted.toString()}
  201. renderItem={this.getMainRenderItem}
  202. />
  203. );
  204. };
  205. getContent() {
  206. const {state} = this;
  207. if (!this.isVoteStarted()) {
  208. return this.getTeaseVoteCard();
  209. }
  210. if (this.isVoteRunning() && !this.hasVoted && !state.hasVoted) {
  211. return this.getVoteCard();
  212. }
  213. if (!this.isResultStarted()) {
  214. return this.getWaitVoteCard();
  215. }
  216. if (this.isResultRunning()) {
  217. return this.getVoteResultCard();
  218. }
  219. return <VoteNotAvailable />;
  220. }
  221. onVoteSuccess = (): void => this.setState({hasVoted: true});
  222. /**
  223. * The user has not voted yet, and the votes are open
  224. */
  225. getVoteCard() {
  226. return (
  227. <VoteSelect
  228. teams={this.teams}
  229. onVoteSuccess={this.onVoteSuccess}
  230. onVoteError={this.reloadData}
  231. />
  232. );
  233. }
  234. /**
  235. * Votes have ended, results can be displayed
  236. */
  237. getVoteResultCard() {
  238. if (this.dates != null && this.datesString != null) {
  239. return (
  240. <VoteResults
  241. teams={this.teams}
  242. dateEnd={this.getDateString(
  243. this.dates.date_result_end,
  244. this.datesString.date_result_end,
  245. )}
  246. />
  247. );
  248. }
  249. return <VoteNotAvailable />;
  250. }
  251. /**
  252. * Vote will open shortly
  253. */
  254. getTeaseVoteCard() {
  255. if (this.dates != null && this.datesString != null) {
  256. return (
  257. <VoteTease
  258. startDate={this.getDateString(
  259. this.dates.date_begin,
  260. this.datesString.date_begin,
  261. )}
  262. />
  263. );
  264. }
  265. return <VoteNotAvailable />;
  266. }
  267. /**
  268. * Votes have ended, or user has voted waiting for results
  269. */
  270. getWaitVoteCard() {
  271. const {state} = this;
  272. let startDate = null;
  273. if (
  274. this.dates != null &&
  275. this.datesString != null &&
  276. this.dates.date_result_begin != null
  277. ) {
  278. startDate = this.getDateString(
  279. this.dates.date_result_begin,
  280. this.datesString.date_result_begin,
  281. );
  282. }
  283. return (
  284. <VoteWait
  285. startDate={startDate}
  286. hasVoted={this.hasVoted || state.hasVoted}
  287. justVoted={state.hasVoted}
  288. isVoteRunning={this.isVoteRunning()}
  289. />
  290. );
  291. }
  292. /**
  293. * Reloads vote data if last refresh delta is smaller than the minimum refresh time
  294. */
  295. reloadData = () => {
  296. let canRefresh;
  297. const {lastRefresh} = this;
  298. if (lastRefresh != null) {
  299. canRefresh =
  300. new Date().getTime() - lastRefresh.getTime() > MIN_REFRESH_TIME;
  301. } else {
  302. canRefresh = true;
  303. }
  304. if (canRefresh && this.authRef.current != null) {
  305. this.authRef.current.reload();
  306. }
  307. };
  308. showMascotDialog = () => {
  309. this.setState({mascotDialogVisible: true});
  310. };
  311. hideMascotDialog = () => {
  312. AsyncStorageManager.set(
  313. AsyncStorageManager.PREFERENCES.voteShowMascot.key,
  314. false,
  315. );
  316. this.setState({mascotDialogVisible: false});
  317. };
  318. isVoteStarted(): boolean {
  319. return this.dates != null && this.today > this.dates.date_begin;
  320. }
  321. isResultRunning(): boolean {
  322. return (
  323. this.dates != null &&
  324. this.today > this.dates.date_result_begin &&
  325. this.today < this.dates.date_result_end
  326. );
  327. }
  328. isResultStarted(): boolean {
  329. return this.dates != null && this.today > this.dates.date_result_begin;
  330. }
  331. isVoteRunning(): boolean {
  332. return (
  333. this.dates != null &&
  334. this.today > this.dates.date_begin &&
  335. this.today < this.dates.date_end
  336. );
  337. }
  338. /**
  339. * Generates the objects containing string and Date representations of key vote dates
  340. */
  341. generateDateObject() {
  342. const strings = this.datesString;
  343. if (strings != null) {
  344. const dateBegin = stringToDate(strings.date_begin);
  345. const dateEnd = stringToDate(strings.date_end);
  346. const dateResultBegin = stringToDate(strings.date_result_begin);
  347. const dateResultEnd = stringToDate(strings.date_result_end);
  348. if (
  349. dateBegin != null &&
  350. dateEnd != null &&
  351. dateResultBegin != null &&
  352. dateResultEnd != null
  353. ) {
  354. this.dates = {
  355. date_begin: dateBegin,
  356. date_end: dateEnd,
  357. date_result_begin: dateResultBegin,
  358. date_result_end: dateResultEnd,
  359. };
  360. } else {
  361. this.dates = null;
  362. }
  363. } else {
  364. this.dates = null;
  365. }
  366. }
  367. /**
  368. * Renders the authenticated screen.
  369. *
  370. * Teams and dates are not mandatory to allow showing the information box even if api requests fail
  371. *
  372. * @returns {*}
  373. */
  374. render() {
  375. const {props, state} = this;
  376. return (
  377. <View style={{flex: 1}}>
  378. <AuthenticatedScreen<TeamResponseType | VoteDatesStringType>
  379. navigation={props.navigation}
  380. ref={this.authRef}
  381. requests={[
  382. {
  383. link: 'elections/teams',
  384. params: {},
  385. mandatory: false,
  386. },
  387. {
  388. link: 'elections/dates',
  389. params: {},
  390. mandatory: false,
  391. },
  392. ]}
  393. renderFunction={this.getScreen}
  394. />
  395. <MascotPopup
  396. visible={state.mascotDialogVisible}
  397. title={i18n.t('screens.vote.mascotDialog.title')}
  398. message={i18n.t('screens.vote.mascotDialog.message')}
  399. icon="vote"
  400. buttons={{
  401. cancel: {
  402. message: i18n.t('screens.vote.mascotDialog.button'),
  403. icon: 'check',
  404. onPress: this.hideMascotDialog,
  405. },
  406. }}
  407. emotion={MASCOT_STYLE.CUTE}
  408. />
  409. </View>
  410. );
  411. }
  412. }