index.tsx 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635
  1. import React, { useEffect, useState, useRef } from 'react';
  2. import { View, Text, TouchableOpacity, ScrollView } from 'react-native';
  3. import ReactModal from 'react-native-modal';
  4. import { useNavigation } from '@react-navigation/native';
  5. import { Picker as WheelPicker } from 'react-native-wheel-pick';
  6. import moment from 'moment';
  7. import ActionSheet from 'react-native-actions-sheet';
  8. import { PageWrapper, Header, Input, WarningModal } from 'src/components';
  9. import RegionItem from '../Components/RegionItem';
  10. import RangeCalendar from 'src/components/Calendars/RangeCalendar';
  11. import { StoreType, storage } from 'src/storage';
  12. import { Colors } from 'src/theme';
  13. import { NAVIGATION_PAGES } from 'src/types';
  14. import { RegionAddData } from '../utils/types';
  15. import {
  16. useGetTripQuery,
  17. usePostDeleteTripMutation,
  18. usePostUpdateTripMutation,
  19. usePostSetNewTripMutation
  20. } from '@api/trips';
  21. import { qualityOptions } from '../utils/constants';
  22. import { styles } from './styles';
  23. import CalendarSvg from '../../../../../assets/icons/calendar.svg';
  24. interface DateValue {
  25. year: number | null;
  26. month: number | null;
  27. day: number | null;
  28. }
  29. interface RegionWithDates extends RegionAddData {
  30. visitStartDate?: DateValue | null;
  31. visitEndDate?: DateValue | null;
  32. year_from?: number;
  33. year_to?: number;
  34. month_from?: number;
  35. month_to?: number;
  36. day_from?: number | null;
  37. day_to?: number | null;
  38. }
  39. interface DatePickerState {
  40. regionId: number;
  41. field: 'visitStartDate' | 'visitEndDate';
  42. }
  43. const AddNewTripScreen = ({ route }: { route: any }) => {
  44. const editTripId = route.params?.editTripId ?? null;
  45. const token = storage.get('token', StoreType.STRING) as string;
  46. const { data: editData } = useGetTripQuery(token, editTripId, Boolean(editTripId));
  47. const navigation = useNavigation();
  48. const [calendarVisible, setCalendarVisible] = useState(false);
  49. const [selectedDates, setSelectedDates] = useState<string | null>(null);
  50. const [description, setDescription] = useState<string>('');
  51. const [regions, setRegions] = useState<RegionWithDates[] | null>(null);
  52. const [disabled, setDisabled] = useState(true);
  53. const [qualitySelectorVisible, setQualitySelectorVisible] = useState(false);
  54. const [selectedRegionId, setSelectedRegionId] = useState<number | null>(null);
  55. const [isWarningModalVisible, setIsWarningModalVisible] = useState(false);
  56. const [showDatePicker, setShowDatePicker] = useState<DatePickerState | null>(null);
  57. const actionSheetRef = useRef<any>(null);
  58. const [selectedYear, setSelectedYear] = useState<number>(new Date().getFullYear());
  59. const [selectedMonth, setSelectedMonth] = useState<number | null>(new Date().getMonth() + 1);
  60. const [selectedDay, setSelectedDay] = useState<number | null>(null);
  61. const { mutate: saveNewTrip } = usePostSetNewTripMutation();
  62. const { mutate: updateTrip } = usePostUpdateTripMutation();
  63. const { mutate: deleteTrip } = usePostDeleteTripMutation();
  64. const fillRegionDatesFromSelectedDates = (regionsToUpdate: RegionWithDates[]) => {
  65. if (!selectedDates || !regionsToUpdate) return regionsToUpdate;
  66. const from = selectedDates.split(' - ')[0];
  67. const to = selectedDates.split(' - ')[1];
  68. const updatedRegions = regionsToUpdate.map((region) => {
  69. const hasEmptyStartDate = !region.visitStartDate?.year || !region.visitStartDate?.month;
  70. const hasEmptyEndDate = !region.visitEndDate?.year || !region.visitEndDate?.month;
  71. if (hasEmptyStartDate || hasEmptyEndDate) {
  72. const updatedRegion = { ...region };
  73. if (hasEmptyStartDate) {
  74. updatedRegion.visitStartDate = {
  75. year: moment(from, 'YYYY-MM-DD').year(),
  76. month: moment(from, 'YYYY-MM-DD').month() + 1,
  77. day: null
  78. };
  79. updatedRegion.year_from = moment(from, 'YYYY-MM-DD').year();
  80. updatedRegion.month_from = moment(from, 'YYYY-MM-DD').month() + 1;
  81. }
  82. if (hasEmptyEndDate) {
  83. updatedRegion.visitEndDate = {
  84. year: moment(to, 'YYYY-MM-DD').year(),
  85. month: moment(to, 'YYYY-MM-DD').month() + 1,
  86. day: null
  87. };
  88. updatedRegion.year_to = moment(to, 'YYYY-MM-DD').year();
  89. updatedRegion.month_to = moment(to, 'YYYY-MM-DD').month() + 1;
  90. }
  91. return updatedRegion;
  92. }
  93. return region;
  94. });
  95. return updatedRegions;
  96. };
  97. useEffect(() => {
  98. if (route.params?.regionsToSave) {
  99. setRegions((currentRegions) => {
  100. const newRegionsIds = route.params.regionsToSave.map((region: RegionAddData) => region.id);
  101. const existingRegions = currentRegions?.filter((region) =>
  102. newRegionsIds.includes(region.id)
  103. );
  104. const updatedRegions = route.params.regionsToSave.map((newRegion: RegionAddData) => {
  105. const existingRegion = existingRegions?.find((region) => region.id === newRegion.id);
  106. return {
  107. ...newRegion,
  108. quality: existingRegion ? existingRegion.quality : 3,
  109. can_be_hidden: existingRegion ? existingRegion.can_be_hidden : newRegion.hidden,
  110. hidden: existingRegion ? existingRegion.hidden : false,
  111. visitStartDate: existingRegion?.visitStartDate || {
  112. year: null,
  113. month: null,
  114. day: null
  115. },
  116. visitEndDate: existingRegion?.visitEndDate || {
  117. year: null,
  118. month: null,
  119. day: null
  120. }
  121. };
  122. });
  123. return fillRegionDatesFromSelectedDates(updatedRegions);
  124. });
  125. }
  126. }, [route.params?.regionsToSave]);
  127. function extractNumberAndExtension(path: string | null) {
  128. if (!path) return null;
  129. const slashIndex = path.lastIndexOf('/');
  130. return path.substring(slashIndex + 1);
  131. }
  132. useEffect(() => {
  133. if (editData && editData.trip) {
  134. setSelectedDates(editData.trip.date_from + ' - ' + editData.trip.date_to);
  135. setDescription(editData.trip.description);
  136. setRegions(
  137. editData.trip.regions.map((region: any) => {
  138. return {
  139. ...region,
  140. id: region.region,
  141. flag1: extractNumberAndExtension(region.flag1),
  142. flag2: extractNumberAndExtension(region.flag2),
  143. visitStartDate: {
  144. year: region.year_from || null,
  145. month: region.month_from || null,
  146. day: region.day_from || null
  147. },
  148. visitEndDate: {
  149. year: region.year_to || null,
  150. month: region.month_to || null,
  151. day: region.day_to || null
  152. }
  153. };
  154. })
  155. );
  156. }
  157. }, [editData]);
  158. useEffect(() => {
  159. if (regions?.length && selectedDates) {
  160. setRegions((currentRegions) => {
  161. if (!currentRegions) return null;
  162. return fillRegionDatesFromSelectedDates(currentRegions);
  163. });
  164. setDisabled(false);
  165. } else {
  166. setDisabled(true);
  167. }
  168. }, [selectedDates]);
  169. useEffect(() => {
  170. setDisabled(!(regions?.length && selectedDates));
  171. }, [regions, selectedDates]);
  172. const currentYear = new Date().getFullYear();
  173. const years = Array.from({ length: 120 }, (_, i) => currentYear - 80 + i);
  174. const getAvailableMonths = (year: number) => {
  175. const allMonths = [
  176. { label: 'Jan', value: 1 },
  177. { label: 'Feb', value: 2 },
  178. { label: 'Mar', value: 3 },
  179. { label: 'Apr', value: 4 },
  180. { label: 'May', value: 5 },
  181. { label: 'Jun', value: 6 },
  182. { label: 'Jul', value: 7 },
  183. { label: 'Aug', value: 8 },
  184. { label: 'Sep', value: 9 },
  185. { label: 'Oct', value: 10 },
  186. { label: 'Nov', value: 11 },
  187. { label: 'Dec', value: 12 }
  188. ];
  189. return allMonths;
  190. };
  191. const months = getAvailableMonths(selectedYear);
  192. const getDaysInMonth = (
  193. year: number,
  194. month: number | null
  195. ): Array<{ label: string; value: number | null }> => {
  196. if (!month) return [{ label: '-', value: null }];
  197. const daysCount = moment(`${year}-${month}`, 'YYYY-M').daysInMonth();
  198. const days = [{ label: '-', value: null }];
  199. for (let i = 1; i <= daysCount; i++) {
  200. days.push({ label: i.toString(), value: i as never });
  201. }
  202. return days;
  203. };
  204. const days = getDaysInMonth(selectedYear, selectedMonth);
  205. const openDatePicker = (
  206. regionId: number,
  207. field: 'visitStartDate' | 'visitEndDate',
  208. initialDate?: DateValue | null
  209. ) => {
  210. setShowDatePicker({ regionId, field });
  211. if (initialDate && initialDate.year && initialDate.month) {
  212. setSelectedYear(initialDate.year);
  213. setSelectedMonth(initialDate.month);
  214. setSelectedDay(initialDate.day || null);
  215. } else {
  216. const today = new Date();
  217. setSelectedYear(today.getFullYear());
  218. setSelectedMonth(today.getMonth() + 1);
  219. setSelectedDay(today.getDate());
  220. }
  221. actionSheetRef.current?.show();
  222. };
  223. const handleDateConfirm = () => {
  224. if (showDatePicker && selectedMonth) {
  225. const dateValue: DateValue = {
  226. year: selectedYear,
  227. month: selectedMonth,
  228. day: selectedDay
  229. };
  230. setRegions(
  231. (prevRegions) =>
  232. prevRegions?.map((region) =>
  233. region.id === showDatePicker.regionId
  234. ? { ...region, [showDatePicker.field]: dateValue }
  235. : region
  236. ) || null
  237. );
  238. setShowDatePicker(null);
  239. actionSheetRef.current?.hide();
  240. }
  241. };
  242. const handleDateCancel = () => {
  243. setShowDatePicker(null);
  244. actionSheetRef.current?.hide();
  245. };
  246. const changeQualityForRegion = (regionId: number | null, newQuality: number) => {
  247. regions &&
  248. setRegions(
  249. regions.map((region) => {
  250. if (region.id === regionId) {
  251. return { ...region, quality: newQuality };
  252. }
  253. return region;
  254. })
  255. );
  256. };
  257. const changeHiddenForRegion = (regionId: number | null) => {
  258. regions &&
  259. setRegions(
  260. regions.map((region) => {
  261. if (region.id === regionId) {
  262. return { ...region, hidden: !region.hidden };
  263. }
  264. return region;
  265. })
  266. );
  267. };
  268. const handleDeleteRegion = (regionId: number) => {
  269. regions && setRegions(regions.filter((region) => region.id !== regionId));
  270. };
  271. const handleDeleteTrip = () => {
  272. setIsWarningModalVisible(false);
  273. deleteTrip(
  274. {
  275. token,
  276. trip_id: editTripId
  277. },
  278. {
  279. onSuccess: () => {
  280. navigation.navigate(...([NAVIGATION_PAGES.TRIPS, { deleted: true }] as never));
  281. }
  282. }
  283. );
  284. };
  285. const handleSaveNewTrip = () => {
  286. if (regions && selectedDates) {
  287. const regionsData = regions.map((region) => {
  288. return {
  289. id: region.id,
  290. quality: region.quality ?? 3,
  291. hidden: region.hidden,
  292. year_from: region.visitStartDate?.year || new Date().getFullYear(),
  293. year_to: region.visitEndDate?.year || new Date().getFullYear(),
  294. month_from: region.visitStartDate?.month || new Date().getMonth() + 1,
  295. month_to: region.visitEndDate?.month || new Date().getMonth() + 1,
  296. day_from: region.visitStartDate?.day || null,
  297. day_to: region.visitEndDate?.day || null
  298. };
  299. });
  300. saveNewTrip(
  301. {
  302. token,
  303. date_from: selectedDates.split(' - ')[0],
  304. date_to: selectedDates.split(' - ')[1],
  305. description,
  306. regions: regionsData
  307. },
  308. {
  309. onSuccess: () => {
  310. navigation.navigate(...([NAVIGATION_PAGES.TRIPS, { saved: true }] as never));
  311. }
  312. }
  313. );
  314. }
  315. };
  316. const handleUpdateTrip = () => {
  317. if (regions && selectedDates) {
  318. const isStartDateInFuture =
  319. selectedDates.split(' - ')[0] > new Date().toISOString().split('T')[0];
  320. const regionsData = regions.map((region) => {
  321. return {
  322. id: region.id,
  323. quality: region.quality ?? 3,
  324. hidden: region.hidden,
  325. year_from: region.visitStartDate?.year || new Date().getFullYear(),
  326. year_to: region.visitEndDate?.year || new Date().getFullYear(),
  327. month_from: region.visitStartDate?.month || new Date().getMonth() + 1,
  328. month_to: region.visitEndDate?.month || new Date().getMonth() + 1,
  329. day_from: region.visitStartDate?.day || null,
  330. day_to: region.visitEndDate?.day || null
  331. };
  332. });
  333. updateTrip(
  334. {
  335. token,
  336. trip_id: editTripId,
  337. date_from: selectedDates.split(' - ')[0],
  338. date_to: selectedDates.split(' - ')[1],
  339. description,
  340. regions: regionsData
  341. },
  342. {
  343. onSuccess: (res) => {
  344. navigation.navigate(...([NAVIGATION_PAGES.TRIPS, { updated: true }] as never));
  345. }
  346. }
  347. );
  348. }
  349. };
  350. return (
  351. <PageWrapper style={{ flex: 1 }}>
  352. <Header label={editTripId ? 'Edit Trip' : 'Add New Trip'} />
  353. <ScrollView
  354. contentContainerStyle={{ flexGrow: 1, gap: 16 }}
  355. showsVerticalScrollIndicator={false}
  356. >
  357. <TouchableOpacity style={styles.regionSelector} onPress={() => setCalendarVisible(true)}>
  358. <CalendarSvg />
  359. <Text style={styles.regionText}>{selectedDates ?? 'Add dates'}</Text>
  360. </TouchableOpacity>
  361. <Input
  362. placeholder="Add description and all interesting moments of your trip"
  363. inputMode={'text'}
  364. onChange={(text) => setDescription(text)}
  365. value={description}
  366. header="Description"
  367. height={54}
  368. multiline={true}
  369. />
  370. <View style={{ marginBottom: 8 }}>
  371. <Text style={styles.regionsLabel}>Regions</Text>
  372. <TouchableOpacity
  373. style={styles.addRegionBtn}
  374. onPress={() =>
  375. navigation.navigate(
  376. ...([
  377. NAVIGATION_PAGES.ADD_REGIONS,
  378. { regionsParams: regions, editId: editTripId }
  379. ] as never)
  380. )
  381. }
  382. >
  383. <Text style={styles.addRegionBtntext}>Add Region</Text>
  384. </TouchableOpacity>
  385. {regions && regions.length ? (
  386. <View style={styles.regionsContainer}>
  387. {regions.map((region) => {
  388. return (
  389. <RegionItem
  390. key={region.id}
  391. region={region}
  392. onDelete={() => handleDeleteRegion(region.id)}
  393. onQualityChange={() => {
  394. setSelectedRegionId(region.id);
  395. setQualitySelectorVisible(true);
  396. }}
  397. onHiddenChange={() => changeHiddenForRegion(region.id)}
  398. openDatePicker={openDatePicker}
  399. visitStartDate={region.visitStartDate}
  400. visitEndDate={region.visitEndDate}
  401. />
  402. );
  403. })}
  404. </View>
  405. ) : (
  406. <Text style={styles.noRegiosText}>No regions at the moment</Text>
  407. )}
  408. </View>
  409. </ScrollView>
  410. <View style={styles.tabContainer}>
  411. {editTripId ? (
  412. <>
  413. <TouchableOpacity
  414. style={[styles.tabStyle, styles.deleteTab]}
  415. onPress={() => setIsWarningModalVisible(true)}
  416. >
  417. <Text style={[styles.tabText, styles.deleteTabText]}>Delete Trip</Text>
  418. </TouchableOpacity>
  419. <TouchableOpacity
  420. style={[
  421. styles.tabStyle,
  422. styles.addNewTab,
  423. disabled && { backgroundColor: Colors.LIGHT_GRAY, borderColor: Colors.LIGHT_GRAY }
  424. ]}
  425. onPress={handleUpdateTrip}
  426. disabled={disabled}
  427. >
  428. <Text style={[styles.tabText, styles.addNewTabText]}>Save Trip</Text>
  429. </TouchableOpacity>
  430. </>
  431. ) : (
  432. <TouchableOpacity
  433. style={[
  434. styles.tabStyle,
  435. styles.addNewTab,
  436. disabled && { backgroundColor: Colors.LIGHT_GRAY, borderColor: Colors.LIGHT_GRAY },
  437. { paddingVertical: 12 }
  438. ]}
  439. onPress={handleSaveNewTrip}
  440. disabled={disabled}
  441. >
  442. <Text style={[styles.tabText, styles.addNewTabText]}>Add New Trip</Text>
  443. </TouchableOpacity>
  444. )}
  445. </View>
  446. <ActionSheet
  447. ref={actionSheetRef}
  448. gestureEnabled={false}
  449. headerAlwaysVisible={true}
  450. CustomHeaderComponent={
  451. <View style={styles.datePickerHeader}>
  452. <TouchableOpacity onPress={handleDateCancel}>
  453. <Text style={styles.datePickerCancel}>Cancel</Text>
  454. </TouchableOpacity>
  455. <Text style={styles.datePickerTitle}>Select Date</Text>
  456. <TouchableOpacity onPress={handleDateConfirm}>
  457. <Text style={styles.datePickerConfirm}>Done</Text>
  458. </TouchableOpacity>
  459. </View>
  460. }
  461. >
  462. <View style={styles.wheelContainer}>
  463. <View style={styles.wheelColumn}>
  464. <Text style={styles.wheelLabel}>Day</Text>
  465. <WheelPicker
  466. style={styles.wheelPicker}
  467. textColor={Colors.DARK_BLUE}
  468. itemStyle={{ fontSize: 16, fontFamily: 'montserrat-600', padding: 0 }}
  469. pickerData={days?.map((d) => d.label)}
  470. selectedValue={days?.find((d) => d.value === selectedDay)?.label || '-'}
  471. onValueChange={(value: string) => {
  472. const day = days?.find((d) => d.label === value);
  473. setSelectedDay(day?.value || null);
  474. }}
  475. />
  476. </View>
  477. <View style={styles.wheelColumn}>
  478. <Text style={styles.wheelLabel}>Month</Text>
  479. <WheelPicker
  480. style={styles.wheelPicker}
  481. textColor={Colors.DARK_BLUE}
  482. itemStyle={{
  483. fontSize: 16,
  484. fontFamily: 'montserrat-600'
  485. }}
  486. pickerData={months ? months?.map((m) => m.label) : []}
  487. selectedValue={months?.find((m) => m.value === selectedMonth)?.label || 'Jan'}
  488. onValueChange={(value: string) => {
  489. const month = months?.find((m) => m.label === value);
  490. setSelectedMonth(month?.value || null);
  491. if (selectedDay && month?.value) {
  492. const maxDaysInMonth = moment(
  493. `${selectedYear}-${month.value}`,
  494. 'YYYY-M'
  495. ).daysInMonth();
  496. if (selectedDay > maxDaysInMonth) {
  497. setSelectedDay(maxDaysInMonth);
  498. }
  499. }
  500. }}
  501. />
  502. </View>
  503. <View style={styles.wheelColumn}>
  504. <Text style={styles.wheelLabel}>Year</Text>
  505. <WheelPicker
  506. style={styles.wheelPicker}
  507. textColor={Colors.DARK_BLUE}
  508. itemStyle={{ fontSize: 16, fontFamily: 'montserrat-600' }}
  509. isCyclic={true}
  510. pickerData={years}
  511. selectedValue={selectedYear}
  512. onValueChange={(value: number) => {
  513. setSelectedYear(value);
  514. if (selectedMonth) {
  515. const maxDaysInMonth = moment(
  516. `${value}-${selectedMonth}`,
  517. 'YYYY-M'
  518. ).daysInMonth();
  519. if (selectedDay && selectedDay > maxDaysInMonth) {
  520. setSelectedDay(maxDaysInMonth);
  521. }
  522. }
  523. }}
  524. />
  525. </View>
  526. </View>
  527. </ActionSheet>
  528. <RangeCalendar
  529. isModalVisible={calendarVisible}
  530. closeModal={(startDate?: string | null, endDate?: string | null) => {
  531. startDate &&
  532. setSelectedDates(
  533. startDate.toString() + ' - ' + (endDate ? endDate?.toString() : startDate?.toString())
  534. );
  535. setCalendarVisible(false);
  536. }}
  537. />
  538. <ReactModal
  539. isVisible={qualitySelectorVisible}
  540. onBackdropPress={() => setQualitySelectorVisible(false)}
  541. style={styles.modal}
  542. statusBarTranslucent={true}
  543. presentationStyle="overFullScreen"
  544. >
  545. <View style={styles.wrapper}>
  546. <View style={{ paddingBottom: 16 }}>
  547. {qualityOptions.map((option) => (
  548. <TouchableOpacity
  549. key={option.id}
  550. style={styles.btnOption}
  551. onPress={() => {
  552. setQualitySelectorVisible(false);
  553. changeQualityForRegion(selectedRegionId, option.id);
  554. }}
  555. >
  556. <Text style={styles.btnOptionText}>{option.name}</Text>
  557. </TouchableOpacity>
  558. ))}
  559. </View>
  560. </View>
  561. </ReactModal>
  562. <WarningModal
  563. type={'delete'}
  564. isVisible={isWarningModalVisible}
  565. onClose={() => setIsWarningModalVisible(false)}
  566. title="Delete Trip"
  567. message="Are you sure you want to delete your trip?"
  568. action={handleDeleteTrip}
  569. />
  570. </PageWrapper>
  571. );
  572. };
  573. export default AddNewTripScreen;