index.tsx 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827
  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. const withDefaultYear =
  198. defaultYear && !editTripId && Number(defaultYear) < CURRENT_YEAR
  199. ? (() => {
  200. const filledIdSet = new Set<number | string>();
  201. return filled.map((r) => {
  202. if (r.visitStartDate?.year) return r;
  203. if (filledIdSet.has(r.id)) return r;
  204. filledIdSet.add(r.id);
  205. return {
  206. ...r,
  207. visitStartDate: { year: Number(defaultYear), month: null, day: null },
  208. visitEndDate: { year: Number(defaultYear), month: null, day: null },
  209. dateMode: 'approx' as CalendarMode
  210. };
  211. });
  212. })()
  213. : filled;
  214. setRegions(withDefaultYear);
  215. setShouldScrollToEmpty(true);
  216. }
  217. }, [route.params?.regionsToSave]);
  218. function extractNumberAndExtension(path: string | null) {
  219. if (!path) return null;
  220. const slashIndex = path.lastIndexOf('/');
  221. return path.substring(slashIndex + 1);
  222. }
  223. useEffect(() => {
  224. if (editData && editData.trip) {
  225. setDescription(editData.trip.description);
  226. setRegions(
  227. editData.trip.regions.map((region: any) => {
  228. const instanceId = `r-${instanceCounterRef.current++}`;
  229. const hasYearOnly = region.year_from && !region.day_from;
  230. return {
  231. ...region,
  232. _instanceId: instanceId,
  233. id: region.region,
  234. flag1: extractNumberAndExtension(region.flag1),
  235. flag2: extractNumberAndExtension(region.flag2),
  236. visitStartDate: {
  237. year: region.year_from || null,
  238. month: region.month_from || null,
  239. day: region.day_from || null
  240. },
  241. visitEndDate: {
  242. year: region.year_to || null,
  243. month: region.month_to || null,
  244. day: region.day_to || null
  245. },
  246. quality: region.quality ?? 3,
  247. dateMode: hasYearOnly ? 'approx' : 'exact'
  248. };
  249. })
  250. );
  251. }
  252. }, [editData]);
  253. useEffect(() => {
  254. setDisabled(!regions?.length);
  255. }, [regions]);
  256. const formatDateForDisplay = (d?: DateValue | null) => {
  257. if (!d || !d.year) return 'Select visit dates';
  258. const m = moment(`${d.year}-${d.month}-${d.day}`, 'YYYY-M-D');
  259. if (!d.day) return m.format('YYYY');
  260. return m.format('D MMM YYYY');
  261. };
  262. const dateValueToISO = (d?: DateValue | null): string | null => {
  263. if (!d?.year) return null;
  264. if (!d.month || !d.day) return `${d.year}-01-01`;
  265. const mm = String(d.month).padStart(2, '0');
  266. const dd = String(d.day).padStart(2, '0');
  267. return `${d.year}-${mm}-${dd}`;
  268. };
  269. const parseISOToDateValue = (iso?: string | null): DateValue | null => {
  270. if (!iso) return null;
  271. const m = moment(iso, 'YYYY-MM-DD');
  272. if (!m.isValid()) return null;
  273. return { year: m.year(), month: m.month() + 1, day: m.date() };
  274. };
  275. const validateRegionsDates = (regionsToValidate?: RegionWithDates[] | null) => {
  276. const list = regionsToValidate ?? regions;
  277. const errors: { [instanceId: string]: string } = {};
  278. if (!list || list.length === 0) {
  279. setRegionErrors(errors);
  280. return false;
  281. }
  282. for (let i = 0; i < list.length; i++) {
  283. const r = list[i];
  284. const s = r.visitStartDate;
  285. const e = r.visitEndDate;
  286. const id = r._instanceId ?? `idx-${i}`;
  287. if (!isValidDate(s)) {
  288. errors[id] = s?.year && s.year >= CURRENT_YEAR
  289. ? 'Current or future year requires a full date'
  290. : 'Please select visit dates';
  291. continue;
  292. }
  293. if (!isValidDate(e)) {
  294. errors[id] = e?.year && e.year >= CURRENT_YEAR
  295. ? 'Current or future year requires a full date'
  296. : 'Please select visit dates';
  297. continue;
  298. }
  299. if (isFullDate(s) && isFullDate(e)) {
  300. const sM = moment(`${s!.year}-${s!.month}-${s!.day}`, 'YYYY-M-D');
  301. const eM = moment(`${e!.year}-${e!.month}-${e!.day}`, 'YYYY-M-D');
  302. if (sM.isAfter(eM)) {
  303. errors[id] = 'Start date cannot be after end date';
  304. continue;
  305. }
  306. } else {
  307. if ((s?.year ?? 0) > (e?.year ?? 0)) {
  308. errors[id] = 'Start year cannot be after end year';
  309. continue;
  310. }
  311. }
  312. }
  313. setRegionErrors(errors);
  314. return Object.keys(errors).length === 0;
  315. };
  316. const openRangeCalendarForRegion = (index: number) => {
  317. if (!regions) return;
  318. setCalendarVisibleForIndex(index);
  319. };
  320. const closeRangeCalendar = (
  321. startDate?: string | null,
  322. endDate?: string | null,
  323. approxYear?: number | null
  324. ) => {
  325. const clickedInstanceIndex = calendarVisibleForIndex;
  326. setCalendarVisibleForIndex(null);
  327. if (clickedInstanceIndex === null || clickedInstanceIndex === undefined) return;
  328. if (!startDate && !approxYear) return;
  329. const openedInstanceId = regions?.[clickedInstanceIndex]?._instanceId;
  330. if (!openedInstanceId) return;
  331. let newStartDate: DateValue;
  332. let newEndDate: DateValue;
  333. let dateMode: CalendarMode;
  334. if (approxYear) {
  335. newStartDate = { year: approxYear, month: null, day: null };
  336. newEndDate = { year: approxYear, month: null, day: null };
  337. dateMode = 'approx';
  338. } else {
  339. const startVal = parseISOToDateValue(startDate);
  340. const endVal = parseISOToDateValue(endDate ?? startDate);
  341. if (!startVal || !endVal) return;
  342. newStartDate = startVal;
  343. newEndDate = endVal;
  344. dateMode = 'exact';
  345. }
  346. const updatedBeforeSort = (regions ?? []).map((r) =>
  347. r._instanceId === openedInstanceId
  348. ? { ...r, visitStartDate: newStartDate, visitEndDate: newEndDate, dateMode }
  349. : r
  350. );
  351. let sorted: RegionWithDates[];
  352. if (dateMode === 'exact') {
  353. const sortKey = (r: RegionWithDates) => dateValueToISO(r.visitStartDate) ?? '9999-12-31';
  354. sorted = [...updatedBeforeSort].sort((a, b) => sortKey(a).localeCompare(sortKey(b)));
  355. const newIndex = sorted.findIndex((r) => r._instanceId === openedInstanceId);
  356. if (newIndex !== -1) {
  357. const next = sorted[newIndex + 1];
  358. if (next && !next.visitStartDate?.year) {
  359. sorted[newIndex + 1] = { ...next, visitStartDate: newEndDate, visitEndDate: newEndDate };
  360. }
  361. const prev = sorted[newIndex - 1];
  362. if (prev && !prev.visitStartDate?.year) {
  363. sorted[newIndex - 1] = { ...prev, visitStartDate: newStartDate, visitEndDate: newStartDate };
  364. }
  365. }
  366. } else {
  367. sorted = updatedBeforeSort;
  368. const editedRegionId = regions?.[clickedInstanceIndex]?.id;
  369. const propagatedIdSet = new Set<number | string>();
  370. propagatedIdSet.add(editedRegionId);
  371. sorted = sorted.map((r: any) => {
  372. if (r._instanceId === openedInstanceId) return r;
  373. if (r.visitStartDate?.year) return r;
  374. if (propagatedIdSet.has(r.id)) return r;
  375. propagatedIdSet.add(r.id);
  376. return {
  377. ...r,
  378. visitStartDate: { year: approxYear, month: null, day: null },
  379. visitEndDate: { year: approxYear, month: null, day: null },
  380. dateMode: 'approx' as CalendarMode
  381. };
  382. });
  383. }
  384. setRegions(sorted);
  385. validateRegionsDates(sorted);
  386. setRegionErrors((prev) => {
  387. const clone = { ...prev };
  388. delete clone[openedInstanceId];
  389. return clone;
  390. });
  391. };
  392. const moveRegionUp = (index: number) => {
  393. if (index <= 0 || !regions) return;
  394. const r = [...regions];
  395. [r[index - 1], r[index]] = [r[index], r[index - 1]];
  396. setRegions(r);
  397. };
  398. const moveRegionDown = (index: number) => {
  399. if (!regions || index >= regions.length - 1) return;
  400. const r = [...regions];
  401. [r[index + 1], r[index]] = [r[index], r[index + 1]];
  402. setRegions(r);
  403. };
  404. const handleDeleteRegion = (index: number) => {
  405. if (!regions) return;
  406. const updated = [...regions];
  407. updated.splice(index, 1);
  408. setRegions(updated);
  409. };
  410. const handleDeleteTrip = async () => {
  411. setIsWarningModalVisible(false);
  412. setPendingDelete(true);
  413. };
  414. const computePayloadDates = (regionsList: RegionWithDates[]) => {
  415. if (!regionsList || regionsList.length === 0) return { date_from: null, date_to: null };
  416. const starts = regionsList
  417. .map((r) => dateValueToISO(r.visitStartDate))
  418. .filter((v): v is string => v !== null);
  419. const ends = regionsList
  420. .map((r) => dateValueToISO(r.visitEndDate))
  421. .filter((v): v is string => v !== null);
  422. if (!starts.length || !ends.length) return { date_from: null, date_to: null };
  423. const toComparableStart = (v: string) =>
  424. v.length === 4 ? `${v}-01-01` : v;
  425. const toComparableEnd = (v: string) =>
  426. v.length === 4 ? `${v}-12-31` : v;
  427. const minStart = starts.reduce((acc, cur) =>
  428. toComparableStart(cur) < toComparableStart(acc) ? cur : acc
  429. );
  430. const maxEnd = ends.reduce((acc, cur) =>
  431. toComparableEnd(cur) > toComparableEnd(acc) ? cur : acc
  432. );
  433. return { date_from: minStart, date_to: maxEnd };
  434. };
  435. const performSaveNewTrip = async () => {
  436. if (!regions) return;
  437. try {
  438. setIsLoading('save');
  439. const regionsData = regions.map((region) => ({
  440. id: region.id,
  441. quality: 3,
  442. hidden: region.hidden,
  443. year_from: region.visitStartDate?.year || null,
  444. year_to: region.visitEndDate?.year || null,
  445. month_from: region.visitStartDate?.month || null,
  446. month_to: region.visitEndDate?.month || null,
  447. day_from: region.visitStartDate?.day || null,
  448. day_to: region.visitEndDate?.day || null
  449. }));
  450. const { date_from, date_to } = computePayloadDates(regions);
  451. saveNewTrip(
  452. { token, date_from, date_to, description, regions: regionsData },
  453. {
  454. onSuccess: (res) => {
  455. console.log('res', res)
  456. if (res && res.result === 'OK') {
  457. navigation.popTo(NAVIGATION_PAGES.TRIPS_2025, { saved: true });
  458. }
  459. setIsLoading(null);
  460. },
  461. onError: (err) => {
  462. setIsLoading(null)
  463. console.log('err', err)
  464. }
  465. }
  466. );
  467. } catch (err) {
  468. console.log('Save trip failed', err);
  469. }
  470. };
  471. const performUpdateTrip = async () => {
  472. if (!regions) return;
  473. try {
  474. setIsLoading('update');
  475. const regionsData = regions.map((region) => ({
  476. id: region.id,
  477. quality: region.quality ?? 3,
  478. hidden: region.hidden,
  479. year_from: region.visitStartDate?.year || null,
  480. year_to: region.visitEndDate?.year || null,
  481. month_from: region.visitStartDate?.month || null,
  482. month_to: region.visitEndDate?.month || null,
  483. day_from: region.visitStartDate?.day || null,
  484. day_to: region.visitEndDate?.day || null
  485. }));
  486. const { date_from, date_to } = computePayloadDates(regions);
  487. updateTrip(
  488. { token, trip_id: editTripId, date_from, date_to, description, regions: regionsData },
  489. {
  490. onSuccess: (res) => {
  491. if (res && res.result === 'OK') {
  492. navigation.popTo(NAVIGATION_PAGES.TRIPS_2025, { updated: true });
  493. }
  494. setIsLoading(null);
  495. },
  496. onError: () => setIsLoading(null)
  497. }
  498. );
  499. } catch (err) {
  500. console.log('Update trip failed', err);
  501. }
  502. };
  503. const handleSaveNewTrip = () => {
  504. if (!regions) return;
  505. if (!validateRegionsDates(regions)) {
  506. scrollToError(regionErrors);
  507. return;
  508. }
  509. const summary = computeTripSummary(regions);
  510. if (summary.totalDays === 0) {
  511. performSaveNewTrip();
  512. return;
  513. }
  514. setSummaryData({ ...summary, perRegion: summary.perRegion?.sort((a: any, b: any) => b.days - a.days) || [] });
  515. setPendingAction('save');
  516. summarySheetRef.current?.show();
  517. };
  518. const handleUpdateTrip = () => {
  519. if (!regions) return;
  520. if (!validateRegionsDates(regions)) {
  521. scrollToError(regionErrors);
  522. return;
  523. }
  524. const summary = computeTripSummary(regions);
  525. if (summary.totalDays === 0) {
  526. performUpdateTrip();
  527. return;
  528. }
  529. setSummaryData({ ...summary, perRegion: summary.perRegion?.sort((a: any, b: any) => b.days - a.days) || [] });
  530. setPendingAction('update');
  531. summarySheetRef.current?.show();
  532. };
  533. const calendarProps = getCalendarProps(calendarVisibleForIndex);
  534. return (
  535. <PageWrapper style={{ flex: 1 }}>
  536. <Header label={editTripId ? 'Edit Trip' : 'Add New Trip'} />
  537. <ScrollView
  538. ref={scrollRef}
  539. contentContainerStyle={{ flexGrow: 1, gap: 16 }}
  540. showsVerticalScrollIndicator={false}
  541. >
  542. <Input
  543. // placeholder="Add description and all interesting moments of your trip"
  544. inputMode={'text'}
  545. onChange={(text) => setDescription(text)}
  546. value={description}
  547. header="Description"
  548. height={36}
  549. multiline={true}
  550. />
  551. <View style={{ marginBottom: 8 }}>
  552. <Text style={styles.regionsLabel}>Regions</Text>
  553. {regions && regions.length ? (
  554. <View style={styles.regionsContainer}>
  555. {regions.map((region, index) => {
  556. const startLabel = formatDateForDisplay(region.visitStartDate);
  557. const endLabel = formatDateForDisplay(region.visitEndDate);
  558. const datesLabel =
  559. region.visitStartDate?.year && region.visitEndDate?.year &&
  560. dateValueToISO(region.visitStartDate) === dateValueToISO(region.visitEndDate)
  561. ? startLabel
  562. : region.visitStartDate?.year && region.visitEndDate?.year
  563. ? `${startLabel} - ${endLabel}`
  564. : 'Select visit dates';
  565. return (
  566. <RegionItem
  567. key={`${region.id}-${index}`}
  568. region={region}
  569. index={index}
  570. total={regions.length}
  571. onDelete={() => handleDeleteRegion(index)}
  572. onSelectDates={() => openRangeCalendarForRegion(index)}
  573. datesLabel={datesLabel}
  574. onMoveUp={() => moveRegionUp(index)}
  575. onMoveDown={() => moveRegionDown(index)}
  576. errorMessage={regionErrors[region?._instanceId ?? index]}
  577. startLabel={startLabel}
  578. endLabel={endLabel}
  579. />
  580. );
  581. })}
  582. </View>
  583. ) : (
  584. <Text style={styles.noRegiosText}>No regions at the moment</Text>
  585. )}
  586. </View>
  587. </ScrollView>
  588. <View style={{ flexDirection: 'column', gap: 6, backgroundColor: 'transparent' }}>
  589. <TouchableOpacity
  590. style={[styles.addRegionBtn]}
  591. onPress={() =>
  592. navigation.navigate(
  593. ...([
  594. NAVIGATION_PAGES.ADD_REGIONS_NEW,
  595. { regionsParams: regions, editId: editTripId, defaultYear, allRegions }
  596. ] as never)
  597. )
  598. }
  599. >
  600. <Text style={styles.addRegionBtntext}>Add new visit</Text>
  601. </TouchableOpacity>
  602. <View style={styles.tabContainer}>
  603. {editTripId ? (
  604. <>
  605. <TouchableOpacity
  606. style={[styles.tabStyle, styles.deleteTab]}
  607. onPress={() => setIsWarningModalVisible(true)}
  608. disabled={isLoading === 'delete'}
  609. >
  610. {isLoading === 'delete' ? (
  611. <ActivityIndicator size={18} color={Colors.RED} />
  612. ) : (
  613. <Text style={[styles.tabText, styles.deleteTabText]}>Delete trip</Text>
  614. )}
  615. </TouchableOpacity>
  616. <TouchableOpacity
  617. style={[
  618. styles.tabStyle,
  619. styles.addNewTab,
  620. disabled && { backgroundColor: Colors.LIGHT_GRAY, borderColor: Colors.LIGHT_GRAY }
  621. ]}
  622. onPress={handleUpdateTrip}
  623. disabled={disabled || isLoading === 'update'}
  624. >
  625. {isLoading === 'update' ? (
  626. <ActivityIndicator size={18} color={Colors.WHITE} />
  627. ) : (
  628. <Text style={[styles.tabText, styles.addNewTabText]}>Save trip</Text>
  629. )}
  630. </TouchableOpacity>
  631. </>
  632. ) : (
  633. <TouchableOpacity
  634. style={[
  635. styles.tabStyle,
  636. styles.addNewTab,
  637. disabled && { backgroundColor: Colors.LIGHT_GRAY, borderColor: Colors.LIGHT_GRAY },
  638. { paddingVertical: 12 }
  639. ]}
  640. onPress={handleSaveNewTrip}
  641. disabled={disabled || isLoading === 'save'}
  642. >
  643. {isLoading === 'save' ? (
  644. <ActivityIndicator size={18} color={Colors.WHITE} />
  645. ) : (
  646. <Text style={[styles.tabText, styles.addNewTabText]}>Save new trip</Text>
  647. )}
  648. </TouchableOpacity>
  649. )}
  650. </View>
  651. </View>
  652. <RangeCalendarWithTabs
  653. isModalVisible={calendarVisibleForIndex !== null}
  654. allowRangeSelection={true}
  655. closeModal={closeRangeCalendar}
  656. defaultMode={calendarProps.defaultMode}
  657. initialApproxYear={calendarProps.initialApproxYear}
  658. initialStartDate={
  659. calendarVisibleForIndex !== null &&
  660. regions?.[calendarVisibleForIndex]?.visitStartDate?.day
  661. ? `${regions[calendarVisibleForIndex].visitStartDate!.year}-${regions[calendarVisibleForIndex].visitStartDate!.month}-${regions[calendarVisibleForIndex].visitStartDate!.day}`
  662. : undefined
  663. }
  664. initialEndDate={
  665. calendarVisibleForIndex !== null &&
  666. regions?.[calendarVisibleForIndex]?.visitEndDate?.day
  667. ? `${regions[calendarVisibleForIndex].visitEndDate!.year}-${regions[calendarVisibleForIndex].visitEndDate!.month}-${regions[calendarVisibleForIndex].visitEndDate!.day}`
  668. : undefined
  669. }
  670. initialYear={
  671. calendarVisibleForIndex !== null
  672. ? regions?.[calendarVisibleForIndex]?.visitStartDate?.year
  673. ?? regions?.find((_, i) => i !== calendarVisibleForIndex && regions[i]?.visitStartDate?.year)
  674. ?.visitStartDate?.year
  675. ?? (defaultYear ? Number(defaultYear) : undefined)
  676. : undefined
  677. }
  678. initialMonth={
  679. calendarVisibleForIndex !== null
  680. ? regions?.[calendarVisibleForIndex]?.visitStartDate?.month ?? undefined
  681. : undefined
  682. }
  683. withHint={true}
  684. />
  685. <WarningModal
  686. type={'delete'}
  687. isVisible={isWarningModalVisible}
  688. onClose={() => setIsWarningModalVisible(false)}
  689. title="Delete Trip"
  690. message="Are you sure you want to delete your trip?"
  691. action={handleDeleteTrip}
  692. onModalHide={() => {
  693. if (pendingDelete) {
  694. setPendingDelete(false);
  695. setIsLoading('delete');
  696. deleteTrip(
  697. {
  698. token,
  699. trip_id: editTripId
  700. },
  701. {
  702. onSuccess: (res) => {
  703. if (res && res.result === 'OK') {
  704. navigation.popTo(
  705. NAVIGATION_PAGES.TRIPS_2025, { deleted: true }
  706. );
  707. }
  708. setIsLoading(null);
  709. },
  710. onError: () => {
  711. setIsLoading(null);
  712. }
  713. }
  714. );
  715. }
  716. }}
  717. />
  718. <SummarySheet
  719. ref={summarySheetRef}
  720. summary={summaryData}
  721. onConfirm={async () => {
  722. summarySheetRef.current?.hide();
  723. isClosingRef.current = true;
  724. }}
  725. onClose={async () => {
  726. if (!isClosingRef.current) return;
  727. if (pendingAction === 'save') performSaveNewTrip();
  728. if (pendingAction === 'update') performUpdateTrip();
  729. isClosingRef.current = false;
  730. }}
  731. />
  732. </PageWrapper>
  733. );
  734. };
  735. export default AddNewTripScreen;