index.tsx 27 KB

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