index.tsx 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771
  1. import React, { useEffect, useState } from 'react';
  2. import { View, Text, TouchableOpacity, ScrollView } from 'react-native';
  3. import { useNavigation } from '@react-navigation/native';
  4. import moment from 'moment';
  5. import { PageWrapper, Header, Input, WarningModal } from 'src/components';
  6. import RegionItem from '../Components/RegionItemNew';
  7. import RangeCalendarWithTabs, { CalendarMode } from 'src/components/Calendars/RangeCalendar/RangeCalendarWithTabs';
  8. import { StoreType, storage } from 'src/storage';
  9. import { Colors } from 'src/theme';
  10. import { NAVIGATION_PAGES } from 'src/types';
  11. import { RegionAddData } from '../utils/types';
  12. import {
  13. useGetTripQuery,
  14. usePostDeleteTripMutation,
  15. usePostUpdateTripMutation,
  16. usePostSetNewTripMutation
  17. } from '@api/trips';
  18. import { styles } from './styles';
  19. import { ActivityIndicator } from 'react-native-paper';
  20. import ActionSheet, { ActionSheetRef } from 'react-native-actions-sheet';
  21. import SummarySheet from '../Components/SummarySheet';
  22. interface DateValue {
  23. year: number | null;
  24. month: number | null;
  25. day: number | null;
  26. }
  27. interface RegionWithDates extends RegionAddData {
  28. _instanceId?: string;
  29. visitStartDate?: DateValue | null;
  30. visitEndDate?: DateValue | null;
  31. year_from?: number;
  32. year_to?: number;
  33. month_from?: number;
  34. month_to?: number;
  35. day_from?: number | null;
  36. day_to?: number | null;
  37. dateMode?: CalendarMode;
  38. }
  39. const CURRENT_YEAR = new Date().getFullYear();
  40. const isFullDate = (d?: DateValue | null): boolean =>
  41. !!(d?.year && d?.month && d?.day);
  42. const isValidDate = (d?: DateValue | null): boolean => {
  43. if (!d?.year) return false;
  44. if (d.year === CURRENT_YEAR) return !!(d.month && d.day);
  45. return true;
  46. };
  47. const AddNewTripScreen = ({ route }: { route: any }) => {
  48. const editTripId = route.params?.editTripId ?? null;
  49. const token = storage.get('token', StoreType.STRING) as string;
  50. const { data: editData } = useGetTripQuery(token, editTripId, Boolean(editTripId));
  51. const navigation = useNavigation();
  52. const [description, setDescription] = useState<string>('');
  53. const [regions, setRegions] = useState<RegionWithDates[] | null>(null);
  54. const [disabled, setDisabled] = useState(true);
  55. const [isLoading, setIsLoading] = useState<string | null>(null);
  56. const [isWarningModalVisible, setIsWarningModalVisible] = useState(false);
  57. const [pendingDelete, setPendingDelete] = useState(false);
  58. const [calendarVisibleForIndex, setCalendarVisibleForIndex] = useState<number | null>(null);
  59. const [regionErrors, setRegionErrors] = useState<{ [instanceId: string]: string }>({});
  60. const [shouldScrollToEmpty, setShouldScrollToEmpty] = useState(false);
  61. const summarySheetRef = React.useRef<ActionSheetRef>(null);
  62. const [summaryData, setSummaryData] = useState({ totalDays: 0, perRegion: [] });
  63. const [pendingAction, setPendingAction] = useState<'save' | 'update' | null>(null);
  64. const isClosingRef = React.useRef<boolean>(null);
  65. const { mutate: saveNewTrip } = usePostSetNewTripMutation();
  66. const { mutate: updateTrip } = usePostUpdateTripMutation();
  67. const { mutate: deleteTrip } = usePostDeleteTripMutation();
  68. const scrollRef = React.useRef<ScrollView>(null);
  69. const instanceCounterRef = React.useRef(1);
  70. const getCalendarProps = (
  71. index: number | null
  72. ): { defaultMode: CalendarMode; initialApproxYear: number | null } => {
  73. if (index === null || !regions?.[index]) {
  74. return { defaultMode: 'exact', initialApproxYear: null };
  75. }
  76. const region = regions[index];
  77. const start = region.visitStartDate;
  78. const isYearOnly = start?.year && !start?.month && start.year !== CURRENT_YEAR;
  79. if (isYearOnly || region.dateMode === 'approx') {
  80. return { defaultMode: 'approx', initialApproxYear: start?.year ?? null };
  81. }
  82. return { defaultMode: 'exact', initialApproxYear: null };
  83. };
  84. const scrollToError = (errors: { [instanceId: string]: string }) => {
  85. if (!regions || !Object.keys(errors).length) return;
  86. const firstInstanceId = Object.keys(errors)[0];
  87. const idx = regions.findIndex((r) => r._instanceId === firstInstanceId);
  88. if (idx === -1) return;
  89. scrollRef.current?.scrollTo({ y: idx * 160, animated: true });
  90. };
  91. const scrollToEmpty = () => {
  92. if (!regions?.length) return;
  93. const idx = regions.findIndex(
  94. (r) => !isValidDate(r.visitStartDate) || !isValidDate(r.visitEndDate)
  95. );
  96. if (idx === -1) return;
  97. scrollRef.current?.scrollTo({ y: idx * 160, animated: true });
  98. };
  99. const computeTripSummary = (list: RegionWithDates[]) => {
  100. const daySet = new Set<string>();
  101. const perRegionMap: Record<string, any> = {};
  102. for (const r of list) {
  103. const isTransit = r.quality === 1;
  104. if (isTransit) {
  105. const key = `${r.id}_transit`;
  106. if (!perRegionMap[key]) {
  107. perRegionMap[key] = {
  108. id: r.id,
  109. name: r.region_name,
  110. days: 0,
  111. transit: true,
  112. flag1: r.flag1!,
  113. flag2: r.flag2 ?? null
  114. };
  115. }
  116. perRegionMap[key].days += 1;
  117. continue;
  118. }
  119. if (!isFullDate(r.visitStartDate) || !isFullDate(r.visitEndDate)) continue;
  120. const startISO = dateValueToISO(r.visitStartDate)!;
  121. const endISO = dateValueToISO(r.visitEndDate)!;
  122. let cursor = moment(startISO);
  123. const end = moment(endISO);
  124. let regionDays = 0;
  125. while (cursor.isSameOrBefore(end)) {
  126. daySet.add(cursor.format('YYYY-MM-DD'));
  127. regionDays++;
  128. cursor = cursor.add(1, 'day');
  129. }
  130. if (!perRegionMap[r.id]) {
  131. perRegionMap[r.id] = { id: r.id, name: r.region_name, days: 0, flag1: r.flag1, flag2: r?.flag2 };
  132. }
  133. perRegionMap[r.id].days += regionDays;
  134. }
  135. return { totalDays: daySet.size, perRegion: Object.values(perRegionMap) };
  136. };
  137. const normalizeRegion = (r: RegionWithDates): RegionWithDates => ({
  138. ...r,
  139. _instanceId: r._instanceId ?? `r-${instanceCounterRef.current++}`,
  140. visitStartDate: r.visitStartDate ? { ...r.visitStartDate } : null,
  141. visitEndDate: r.visitEndDate ? { ...r.visitEndDate } : null
  142. });
  143. const autoFillAfterAppend = (list: RegionWithDates[], oldCount: number) => {
  144. if (!list || list.length === 0) return list;
  145. const updated = [...list];
  146. let lastWithDateIndex: number | null = null;
  147. for (let i = updated.length - 1; i >= 0; i--) {
  148. const r = updated[i];
  149. r._instanceId = `r-${instanceCounterRef.current++}`;
  150. if (isFullDate(r.visitStartDate) && isFullDate(r.visitEndDate)) {
  151. lastWithDateIndex = i;
  152. break;
  153. }
  154. }
  155. if (lastWithDateIndex === null || lastWithDateIndex < oldCount - 1) return updated;
  156. const lastDate: DateValue = updated[lastWithDateIndex].visitEndDate as DateValue;
  157. for (let i = lastWithDateIndex + 1; i < updated.length; i++) {
  158. const r = updated[i];
  159. if (!isFullDate(r.visitStartDate) || !isFullDate(r.visitEndDate)) {
  160. updated[i] = {
  161. ...r,
  162. _instanceId: `r-${instanceCounterRef.current++}`,
  163. visitStartDate: lastDate,
  164. visitEndDate: lastDate
  165. };
  166. break;
  167. }
  168. }
  169. return updated;
  170. };
  171. useEffect(() => {
  172. if (!shouldScrollToEmpty || !regions) return;
  173. requestAnimationFrame(() => {
  174. scrollToEmpty();
  175. setShouldScrollToEmpty(false);
  176. });
  177. }, [shouldScrollToEmpty, regions]);
  178. useEffect(() => {
  179. if (route.params?.regionsToSave) {
  180. const oldRegions = route.params.regionsToSave.filter((r: any) => r._instanceId);
  181. const oldCount = oldRegions.length;
  182. const normalized = route.params.regionsToSave.map(normalizeRegion);
  183. const filled = autoFillAfterAppend(normalized, oldCount);
  184. setRegions(filled);
  185. setShouldScrollToEmpty(true);
  186. }
  187. }, [route.params?.regionsToSave]);
  188. function extractNumberAndExtension(path: string | null) {
  189. if (!path) return null;
  190. const slashIndex = path.lastIndexOf('/');
  191. return path.substring(slashIndex + 1);
  192. }
  193. useEffect(() => {
  194. if (editData && editData.trip) {
  195. setDescription(editData.trip.description);
  196. setRegions(
  197. editData.trip.regions.map((region: any) => {
  198. const instanceId = `r-${instanceCounterRef.current++}`;
  199. const hasYearOnly = region.year_from && !region.day_from;
  200. return {
  201. ...region,
  202. _instanceId: instanceId,
  203. id: region.region,
  204. flag1: extractNumberAndExtension(region.flag1),
  205. flag2: extractNumberAndExtension(region.flag2),
  206. visitStartDate: {
  207. year: region.year_from || null,
  208. month: region.month_from || null,
  209. day: region.day_from || null
  210. },
  211. visitEndDate: {
  212. year: region.year_to || null,
  213. month: region.month_to || null,
  214. day: region.day_to || null
  215. },
  216. quality: region.quality ?? 3,
  217. dateMode: hasYearOnly ? 'approx' : 'exact'
  218. };
  219. })
  220. );
  221. }
  222. }, [editData]);
  223. useEffect(() => {
  224. setDisabled(!regions?.length);
  225. }, [regions]);
  226. const formatDateForDisplay = (d?: DateValue | null) => {
  227. if (!d || !d.year) return 'Select dates';
  228. const m = moment(`${d.year}-${d.month}-${d.day}`, 'YYYY-M-D');
  229. return !d.month ? m.format('YYYY') : !d.day ? m.format('MMM YYYY') : m.format('D MMM YYYY');
  230. };
  231. const dateValueToISO = (d?: DateValue | null): string | null => {
  232. if (!d?.year) return null;
  233. if (!d.month || !d.day) return `${d.year}-01-01`;
  234. const mm = String(d.month).padStart(2, '0');
  235. const dd = String(d.day).padStart(2, '0');
  236. return `${d.year}-${mm}-${dd}`;
  237. };
  238. const parseISOToDateValue = (iso?: string | null): DateValue | null => {
  239. if (!iso) return null;
  240. const m = moment(iso, 'YYYY-MM-DD');
  241. if (!m.isValid()) return null;
  242. return { year: m.year(), month: m.month() + 1, day: m.date() };
  243. };
  244. const validateRegionsDates = (regionsToValidate?: RegionWithDates[] | null) => {
  245. const list = regionsToValidate ?? regions;
  246. const errors: { [instanceId: string]: string } = {};
  247. if (!list || list.length === 0) {
  248. setRegionErrors(errors);
  249. return false;
  250. }
  251. for (let i = 0; i < list.length; i++) {
  252. const r = list[i];
  253. const s = r.visitStartDate;
  254. const e = r.visitEndDate;
  255. const id = r._instanceId ?? `idx-${i}`;
  256. if (!isValidDate(s)) {
  257. errors[id] = s?.year === CURRENT_YEAR
  258. ? 'Current year requires a full date'
  259. : 'Please select visit dates';
  260. continue;
  261. }
  262. if (!isValidDate(e)) {
  263. errors[id] = e?.year === CURRENT_YEAR
  264. ? 'Current year requires a full date'
  265. : 'Please select visit dates';
  266. continue;
  267. }
  268. if (isFullDate(s) && isFullDate(e)) {
  269. const sM = moment(`${s!.year}-${s!.month}-${s!.day}`, 'YYYY-M-D');
  270. const eM = moment(`${e!.year}-${e!.month}-${e!.day}`, 'YYYY-M-D');
  271. if (sM.isAfter(eM)) {
  272. errors[id] = 'Start date cannot be after end date';
  273. continue;
  274. }
  275. } else {
  276. if ((s?.year ?? 0) > (e?.year ?? 0)) {
  277. errors[id] = 'Start year cannot be after end year';
  278. continue;
  279. }
  280. }
  281. }
  282. setRegionErrors(errors);
  283. return Object.keys(errors).length === 0;
  284. };
  285. const openRangeCalendarForRegion = (index: number) => {
  286. if (!regions) return;
  287. setCalendarVisibleForIndex(index);
  288. };
  289. const closeRangeCalendar = (
  290. startDate?: string | null,
  291. endDate?: string | null,
  292. approxYear?: number | null
  293. ) => {
  294. const clickedInstanceIndex = calendarVisibleForIndex;
  295. setCalendarVisibleForIndex(null);
  296. if (clickedInstanceIndex === null || clickedInstanceIndex === undefined) return;
  297. if (!startDate && !approxYear) return;
  298. const openedInstanceId = regions?.[clickedInstanceIndex]?._instanceId;
  299. if (!openedInstanceId) return;
  300. let newStartDate: DateValue;
  301. let newEndDate: DateValue;
  302. let dateMode: CalendarMode;
  303. if (approxYear) {
  304. newStartDate = { year: approxYear, month: null, day: null };
  305. newEndDate = { year: approxYear, month: null, day: null };
  306. dateMode = 'approx';
  307. } else {
  308. const startVal = parseISOToDateValue(startDate);
  309. const endVal = parseISOToDateValue(endDate ?? startDate);
  310. if (!startVal || !endVal) return;
  311. newStartDate = startVal;
  312. newEndDate = endVal;
  313. dateMode = 'exact';
  314. }
  315. const updatedBeforeSort = (regions ?? []).map((r) =>
  316. r._instanceId === openedInstanceId
  317. ? { ...r, visitStartDate: newStartDate, visitEndDate: newEndDate, dateMode }
  318. : r
  319. );
  320. let sorted: RegionWithDates[];
  321. if (dateMode === 'exact') {
  322. const sortKey = (r: RegionWithDates) => dateValueToISO(r.visitStartDate) ?? '9999-12-31';
  323. sorted = [...updatedBeforeSort].sort((a, b) => sortKey(a).localeCompare(sortKey(b)));
  324. const newIndex = sorted.findIndex((r) => r._instanceId === openedInstanceId);
  325. if (newIndex !== -1) {
  326. const next = sorted[newIndex + 1];
  327. if (next && !isFullDate(next.visitStartDate)) {
  328. sorted[newIndex + 1] = { ...next, visitStartDate: newEndDate, visitEndDate: newEndDate };
  329. }
  330. const prev = sorted[newIndex - 1];
  331. if (prev && !isFullDate(prev.visitStartDate)) {
  332. sorted[newIndex - 1] = { ...prev, visitStartDate: newStartDate, visitEndDate: newStartDate };
  333. }
  334. }
  335. } else {
  336. sorted = updatedBeforeSort;
  337. }
  338. setRegions(sorted);
  339. validateRegionsDates(sorted);
  340. setRegionErrors((prev) => {
  341. const clone = { ...prev };
  342. delete clone[openedInstanceId];
  343. return clone;
  344. });
  345. };
  346. const moveRegionUp = (index: number) => {
  347. if (index <= 0 || !regions) return;
  348. const r = [...regions];
  349. [r[index - 1], r[index]] = [r[index], r[index - 1]];
  350. setRegions(r);
  351. };
  352. const moveRegionDown = (index: number) => {
  353. if (!regions || index >= regions.length - 1) return;
  354. const r = [...regions];
  355. [r[index + 1], r[index]] = [r[index], r[index + 1]];
  356. setRegions(r);
  357. };
  358. const handleDeleteRegion = (index: number) => {
  359. if (!regions) return;
  360. const updated = [...regions];
  361. updated.splice(index, 1);
  362. setRegions(updated);
  363. };
  364. const handleDeleteTrip = async () => {
  365. setIsWarningModalVisible(false);
  366. setPendingDelete(true);
  367. };
  368. const computePayloadDates = (regionsList: RegionWithDates[]) => {
  369. if (!regionsList || regionsList.length === 0) return { date_from: null, date_to: null };
  370. const starts = regionsList
  371. .map((r) => dateValueToISO(r.visitStartDate))
  372. .filter((v): v is string => v !== null);
  373. const ends = regionsList
  374. .map((r) => dateValueToISO(r.visitEndDate))
  375. .filter((v): v is string => v !== null);
  376. if (!starts.length || !ends.length) return { date_from: null, date_to: null };
  377. const toComparableStart = (v: string) =>
  378. v.length === 4 ? `${v}-01-01` : v;
  379. const toComparableEnd = (v: string) =>
  380. v.length === 4 ? `${v}-12-31` : v;
  381. const minStart = starts.reduce((acc, cur) =>
  382. toComparableStart(cur) < toComparableStart(acc) ? cur : acc
  383. );
  384. const maxEnd = ends.reduce((acc, cur) =>
  385. toComparableEnd(cur) > toComparableEnd(acc) ? cur : acc
  386. );
  387. return { date_from: minStart, date_to: maxEnd };
  388. };
  389. const performSaveNewTrip = async () => {
  390. if (!regions) return;
  391. try {
  392. setIsLoading('save');
  393. const regionsData = regions.map((region) => ({
  394. id: region.id,
  395. quality: 3,
  396. hidden: region.hidden,
  397. year_from: region.visitStartDate?.year || null,
  398. year_to: region.visitEndDate?.year || null,
  399. month_from: region.visitStartDate?.month || null,
  400. month_to: region.visitEndDate?.month || null,
  401. day_from: region.visitStartDate?.day || null,
  402. day_to: region.visitEndDate?.day || null
  403. }));
  404. const { date_from, date_to } = computePayloadDates(regions);
  405. saveNewTrip(
  406. { token, date_from, date_to, description, regions: regionsData },
  407. {
  408. onSuccess: (res) => {
  409. console.log('res', res)
  410. if (res && res.result === 'OK') {
  411. navigation.popTo(...([NAVIGATION_PAGES.TRIPS_2025, { saved: true }] as never));
  412. }
  413. setIsLoading(null);
  414. },
  415. onError: (err) => {
  416. setIsLoading(null)
  417. console.log('err', err)
  418. }
  419. }
  420. );
  421. } catch (err) {
  422. console.log('Save trip failed', err);
  423. }
  424. };
  425. const performUpdateTrip = async () => {
  426. if (!regions) return;
  427. try {
  428. setIsLoading('update');
  429. const regionsData = regions.map((region) => ({
  430. id: region.id,
  431. quality: region.quality ?? 3,
  432. hidden: region.hidden,
  433. year_from: region.visitStartDate?.year || null,
  434. year_to: region.visitEndDate?.year || null,
  435. month_from: region.visitStartDate?.month || null,
  436. month_to: region.visitEndDate?.month || null,
  437. day_from: region.visitStartDate?.day || null,
  438. day_to: region.visitEndDate?.day || null
  439. }));
  440. const { date_from, date_to } = computePayloadDates(regions);
  441. updateTrip(
  442. { token, trip_id: editTripId, date_from, date_to, description, regions: regionsData },
  443. {
  444. onSuccess: (res) => {
  445. if (res && res.result === 'OK') {
  446. navigation.popTo(...([NAVIGATION_PAGES.TRIPS_2025, { updated: true }] as never));
  447. }
  448. setIsLoading(null);
  449. },
  450. onError: () => setIsLoading(null)
  451. }
  452. );
  453. } catch (err) {
  454. console.log('Update trip failed', err);
  455. }
  456. };
  457. const handleSaveNewTrip = () => {
  458. if (!regions) return;
  459. if (!validateRegionsDates(regions)) {
  460. scrollToError(regionErrors);
  461. return;
  462. }
  463. const summary = computeTripSummary(regions);
  464. if (summary.totalDays === 0) {
  465. performSaveNewTrip();
  466. return;
  467. }
  468. setSummaryData({ ...summary, perRegion: summary.perRegion?.sort((a: any, b: any) => b.days - a.days) || [] });
  469. setPendingAction('save');
  470. summarySheetRef.current?.show();
  471. };
  472. const handleUpdateTrip = () => {
  473. if (!regions) return;
  474. if (!validateRegionsDates(regions)) {
  475. scrollToError(regionErrors);
  476. return;
  477. }
  478. const summary = computeTripSummary(regions);
  479. if (summary.totalDays === 0) {
  480. performUpdateTrip();
  481. return;
  482. }
  483. setSummaryData({ ...summary, perRegion: summary.perRegion?.sort((a: any, b: any) => b.days - a.days) || [] });
  484. setPendingAction('update');
  485. summarySheetRef.current?.show();
  486. };
  487. const calendarProps = getCalendarProps(calendarVisibleForIndex);
  488. return (
  489. <PageWrapper style={{ flex: 1 }}>
  490. <Header label={editTripId ? 'Edit Trip' : 'Add New Trip'} />
  491. <ScrollView
  492. ref={scrollRef}
  493. contentContainerStyle={{ flexGrow: 1, gap: 16 }}
  494. showsVerticalScrollIndicator={false}
  495. >
  496. <Input
  497. // placeholder="Add description and all interesting moments of your trip"
  498. inputMode={'text'}
  499. onChange={(text) => setDescription(text)}
  500. value={description}
  501. header="Description"
  502. height={36}
  503. multiline={true}
  504. />
  505. <View style={{ marginBottom: 8 }}>
  506. <Text style={styles.regionsLabel}>Regions</Text>
  507. {regions && regions.length ? (
  508. <View style={styles.regionsContainer}>
  509. {regions.map((region, index) => {
  510. const startLabel = formatDateForDisplay(region.visitStartDate);
  511. const endLabel = formatDateForDisplay(region.visitEndDate);
  512. const datesLabel =
  513. region.visitStartDate?.year && region.visitEndDate?.year &&
  514. dateValueToISO(region.visitStartDate) === dateValueToISO(region.visitEndDate)
  515. ? startLabel
  516. : region.visitStartDate?.year && region.visitEndDate?.year
  517. ? `${startLabel} - ${endLabel}`
  518. : 'Select visit dates';
  519. return (
  520. <RegionItem
  521. key={`${region.id}-${index}`}
  522. region={region}
  523. index={index}
  524. total={regions.length}
  525. onDelete={() => handleDeleteRegion(index)}
  526. onSelectDates={() => openRangeCalendarForRegion(index)}
  527. datesLabel={datesLabel}
  528. onMoveUp={() => moveRegionUp(index)}
  529. onMoveDown={() => moveRegionDown(index)}
  530. errorMessage={regionErrors[region?._instanceId ?? index]}
  531. startLabel={startLabel}
  532. endLabel={endLabel}
  533. />
  534. );
  535. })}
  536. </View>
  537. ) : (
  538. <Text style={styles.noRegiosText}>No regions at the moment</Text>
  539. )}
  540. </View>
  541. </ScrollView>
  542. <View style={{ flexDirection: 'column', gap: 6, backgroundColor: 'transparent' }}>
  543. <TouchableOpacity
  544. style={[styles.addRegionBtn]}
  545. onPress={() =>
  546. navigation.navigate(
  547. ...([
  548. NAVIGATION_PAGES.ADD_REGIONS_NEW,
  549. { regionsParams: regions, editId: editTripId }
  550. ] as never)
  551. )
  552. }
  553. >
  554. <Text style={styles.addRegionBtntext}>Add new visit</Text>
  555. </TouchableOpacity>
  556. <View style={styles.tabContainer}>
  557. {editTripId ? (
  558. <>
  559. <TouchableOpacity
  560. style={[styles.tabStyle, styles.deleteTab]}
  561. onPress={() => setIsWarningModalVisible(true)}
  562. disabled={isLoading === 'delete'}
  563. >
  564. {isLoading === 'delete' ? (
  565. <ActivityIndicator size={18} color={Colors.RED} />
  566. ) : (
  567. <Text style={[styles.tabText, styles.deleteTabText]}>Delete trip</Text>
  568. )}
  569. </TouchableOpacity>
  570. <TouchableOpacity
  571. style={[
  572. styles.tabStyle,
  573. styles.addNewTab,
  574. disabled && { backgroundColor: Colors.LIGHT_GRAY, borderColor: Colors.LIGHT_GRAY }
  575. ]}
  576. onPress={handleUpdateTrip}
  577. disabled={disabled || isLoading === 'update'}
  578. >
  579. {isLoading === 'update' ? (
  580. <ActivityIndicator size={18} color={Colors.WHITE} />
  581. ) : (
  582. <Text style={[styles.tabText, styles.addNewTabText]}>Save trip</Text>
  583. )}
  584. </TouchableOpacity>
  585. </>
  586. ) : (
  587. <TouchableOpacity
  588. style={[
  589. styles.tabStyle,
  590. styles.addNewTab,
  591. disabled && { backgroundColor: Colors.LIGHT_GRAY, borderColor: Colors.LIGHT_GRAY },
  592. { paddingVertical: 12 }
  593. ]}
  594. onPress={handleSaveNewTrip}
  595. disabled={disabled || isLoading === 'save'}
  596. >
  597. {isLoading === 'save' ? (
  598. <ActivityIndicator size={18} color={Colors.WHITE} />
  599. ) : (
  600. <Text style={[styles.tabText, styles.addNewTabText]}>Save new trip</Text>
  601. )}
  602. </TouchableOpacity>
  603. )}
  604. </View>
  605. </View>
  606. <RangeCalendarWithTabs
  607. isModalVisible={calendarVisibleForIndex !== null}
  608. allowRangeSelection={true}
  609. closeModal={closeRangeCalendar}
  610. defaultMode={calendarProps.defaultMode}
  611. initialApproxYear={calendarProps.initialApproxYear}
  612. initialStartDate={
  613. calendarVisibleForIndex !== null &&
  614. regions?.[calendarVisibleForIndex]?.visitStartDate?.day
  615. ? `${regions[calendarVisibleForIndex].visitStartDate!.year}-${regions[calendarVisibleForIndex].visitStartDate!.month}-${regions[calendarVisibleForIndex].visitStartDate!.day}`
  616. : undefined
  617. }
  618. initialEndDate={
  619. calendarVisibleForIndex !== null &&
  620. regions?.[calendarVisibleForIndex]?.visitEndDate?.day
  621. ? `${regions[calendarVisibleForIndex].visitEndDate!.year}-${regions[calendarVisibleForIndex].visitEndDate!.month}-${regions[calendarVisibleForIndex].visitEndDate!.day}`
  622. : undefined
  623. }
  624. initialYear={
  625. calendarVisibleForIndex !== null
  626. ? regions?.[calendarVisibleForIndex]?.visitStartDate?.year ?? undefined
  627. : undefined
  628. }
  629. initialMonth={
  630. calendarVisibleForIndex !== null
  631. ? regions?.[calendarVisibleForIndex]?.visitStartDate?.month ?? undefined
  632. : undefined
  633. }
  634. withHint={true}
  635. />
  636. <WarningModal
  637. type={'delete'}
  638. isVisible={isWarningModalVisible}
  639. onClose={() => setIsWarningModalVisible(false)}
  640. title="Delete Trip"
  641. message="Are you sure you want to delete your trip?"
  642. action={handleDeleteTrip}
  643. onModalHide={() => {
  644. if (pendingDelete) {
  645. setPendingDelete(false);
  646. setIsLoading('delete');
  647. deleteTrip(
  648. {
  649. token,
  650. trip_id: editTripId
  651. },
  652. {
  653. onSuccess: (res) => {
  654. if (res && res.result === 'OK') {
  655. navigation.popTo(
  656. ...([NAVIGATION_PAGES.TRIPS_2025, { deleted: true }] as never)
  657. );
  658. }
  659. setIsLoading(null);
  660. },
  661. onError: () => {
  662. setIsLoading(null);
  663. }
  664. }
  665. );
  666. }
  667. }}
  668. />
  669. <SummarySheet
  670. ref={summarySheetRef}
  671. summary={summaryData}
  672. onConfirm={async () => {
  673. summarySheetRef.current?.hide();
  674. isClosingRef.current = true;
  675. }}
  676. onClose={async () => {
  677. if (!isClosingRef.current) return;
  678. if (pendingAction === 'save') performSaveNewTrip();
  679. if (pendingAction === 'update') performUpdateTrip();
  680. isClosingRef.current = false;
  681. }}
  682. />
  683. </PageWrapper>
  684. );
  685. };
  686. export default AddNewTripScreen;