import React, { useEffect, useState } from 'react'; import { View, Text, TouchableOpacity, ScrollView } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import moment from 'moment'; import { PageWrapper, Header, Input, WarningModal } from 'src/components'; import RegionItem from '../Components/RegionItemNew'; import RangeCalendarWithTabs, { CalendarMode } from 'src/components/Calendars/RangeCalendar/RangeCalendarWithTabs'; import { StoreType, storage } from 'src/storage'; import { Colors } from 'src/theme'; import { NAVIGATION_PAGES } from 'src/types'; import { RegionAddData } from '../utils/types'; import { useGetTripQuery, usePostDeleteTripMutation, usePostUpdateTripMutation, usePostSetNewTripMutation, useGetRegionsForTripsQuery } from '@api/trips'; import { styles } from './styles'; import { ActivityIndicator } from 'react-native-paper'; import { ActionSheetRef } from 'react-native-actions-sheet'; import SummarySheet from '../Components/SummarySheet'; interface DateValue { year: number | null; month: number | null; day: number | null; } interface RegionWithDates extends RegionAddData { _instanceId?: string; visitStartDate?: DateValue | null; visitEndDate?: DateValue | null; year_from?: number; year_to?: number; month_from?: number; month_to?: number; day_from?: number | null; day_to?: number | null; dateMode?: CalendarMode; } const CURRENT_YEAR = new Date().getFullYear(); const isFullDate = (d?: DateValue | null): boolean => !!(d?.year && d?.month && d?.day); const isValidDate = (d?: DateValue | null): boolean => { if (!d?.year) return false; if (d.year >= CURRENT_YEAR) return !!(d.month && d.day); return true; }; const AddNewTripScreen = ({ route }: { route: any }) => { const editTripId = route.params?.editTripId ?? null; const token = storage.get('token', StoreType.STRING) as string; const { data: editData } = useGetTripQuery(token, editTripId, Boolean(editTripId)); const navigation = useNavigation(); const { defaultRegion, defaultYear } = route.params; const [allRegions, setAllRegions] = useState([]); const { data } = useGetRegionsForTripsQuery(true); const [description, setDescription] = useState(''); const [regions, setRegions] = useState(null); const [disabled, setDisabled] = useState(true); const [isLoading, setIsLoading] = useState(null); const [isWarningModalVisible, setIsWarningModalVisible] = useState(false); const [pendingDelete, setPendingDelete] = useState(false); const [calendarVisibleForIndex, setCalendarVisibleForIndex] = useState(null); const [regionErrors, setRegionErrors] = useState<{ [instanceId: string]: string }>({}); const [shouldScrollToEmpty, setShouldScrollToEmpty] = useState(false); const summarySheetRef = React.useRef(null); const [summaryData, setSummaryData] = useState({ totalDays: 0, perRegion: [] }); const [pendingAction, setPendingAction] = useState<'save' | 'update' | null>(null); const isClosingRef = React.useRef(null); const { mutate: saveNewTrip } = usePostSetNewTripMutation(); const { mutate: updateTrip } = usePostUpdateTripMutation(); const { mutate: deleteTrip } = usePostDeleteTripMutation(); const scrollRef = React.useRef(null); const instanceCounterRef = React.useRef(1); useEffect(() => { if (data && data.regions) { setAllRegions(data.regions); if (defaultRegion && !editTripId && !route.params?.regionsToSave) { const regionFromApi = data.regions?.find((r) => r.id === defaultRegion.id); regionFromApi && setRegions([normalizeRegion(regionFromApi)]); } } }, [data]); const getCalendarProps = ( index: number | null ): { defaultMode: CalendarMode; initialApproxYear: number | null } => { if (index === null || !regions?.[index]) { return { defaultMode: 'exact', initialApproxYear: null }; } const region = regions[index]; const start = region.visitStartDate; const isYearOnly = start?.year && !start?.month && start.year !== CURRENT_YEAR; if (isYearOnly || region.dateMode === 'approx') { return { defaultMode: 'approx', initialApproxYear: start?.year ?? null }; } return { defaultMode: 'exact', initialApproxYear: null }; }; const scrollToError = (errors: { [instanceId: string]: string }) => { if (!regions || !Object.keys(errors).length) return; const firstInstanceId = Object.keys(errors)[0]; const idx = regions.findIndex((r) => r._instanceId === firstInstanceId); if (idx === -1) return; scrollRef.current?.scrollTo({ y: idx * 160, animated: true }); }; const scrollToEmpty = () => { if (!regions?.length) return; const idx = regions.findIndex( (r) => !isValidDate(r.visitStartDate) || !isValidDate(r.visitEndDate) ); if (idx === -1) return; scrollRef.current?.scrollTo({ y: idx * 160, animated: true }); }; const computeTripSummary = (list: RegionWithDates[]) => { const daySet = new Set(); const perRegionMap: Record = {}; for (const r of list) { const isTransit = r.quality === 1; if (isTransit) { const key = `${r.id}_transit`; if (!perRegionMap[key]) { perRegionMap[key] = { id: r.id, name: r.region_name, days: 0, transit: true, flag1: r.flag1!, flag2: r.flag2 ?? null }; } perRegionMap[key].days += 1; continue; } if (!isFullDate(r.visitStartDate) || !isFullDate(r.visitEndDate)) continue; const startISO = dateValueToISO(r.visitStartDate)!; const endISO = dateValueToISO(r.visitEndDate)!; let cursor = moment(startISO); const end = moment(endISO); let regionDays = 0; while (cursor.isSameOrBefore(end)) { daySet.add(cursor.format('YYYY-MM-DD')); regionDays++; cursor = cursor.add(1, 'day'); } if (!perRegionMap[r.id]) { perRegionMap[r.id] = { id: r.id, name: r.region_name, days: 0, flag1: r.flag1, flag2: r?.flag2 }; } perRegionMap[r.id].days += regionDays; } return { totalDays: daySet.size, perRegion: Object.values(perRegionMap) }; }; const normalizeRegion = (r: RegionWithDates): RegionWithDates => ({ ...r, _instanceId: r._instanceId ?? `r-${instanceCounterRef.current++}`, visitStartDate: r.visitStartDate ? { ...r.visitStartDate } : null, visitEndDate: r.visitEndDate ? { ...r.visitEndDate } : null }); const autoFillAfterAppend = (list: RegionWithDates[], oldCount: number) => { if (!list || list.length === 0) return list; const updated = [...list]; let lastWithDateIndex: number | null = null; for (let i = updated.length - 1; i >= 0; i--) { const r = updated[i]; r._instanceId = `r-${instanceCounterRef.current++}`; if (isFullDate(r.visitStartDate) && isFullDate(r.visitEndDate)) { lastWithDateIndex = i; break; } } if (lastWithDateIndex === null || lastWithDateIndex < oldCount - 1) return updated; const lastDate: DateValue = updated[lastWithDateIndex].visitEndDate as DateValue; for (let i = lastWithDateIndex + 1; i < updated.length; i++) { const r = updated[i]; if (!isFullDate(r.visitStartDate) || !isFullDate(r.visitEndDate)) { updated[i] = { ...r, _instanceId: `r-${instanceCounterRef.current++}`, visitStartDate: lastDate, visitEndDate: lastDate }; break; } } return updated; }; useEffect(() => { if (!shouldScrollToEmpty || !regions) return; requestAnimationFrame(() => { scrollToEmpty(); setShouldScrollToEmpty(false); }); }, [shouldScrollToEmpty, regions]); useEffect(() => { if (route.params?.regionsToSave) { const oldRegions = route.params.regionsToSave.filter((r: any) => r._instanceId); const oldCount = oldRegions.length; const normalized = route.params.regionsToSave.map(normalizeRegion); const filled = autoFillAfterAppend(normalized, oldCount); const withDefaultYear = defaultYear && !editTripId && Number(defaultYear) < CURRENT_YEAR ? (() => { const filledIdSet = new Set(); return filled.map((r) => { if (r.visitStartDate?.year) return r; if (filledIdSet.has(r.id)) return r; filledIdSet.add(r.id); return { ...r, visitStartDate: { year: Number(defaultYear), month: null, day: null }, visitEndDate: { year: Number(defaultYear), month: null, day: null }, dateMode: 'approx' as CalendarMode }; }); })() : filled; setRegions(withDefaultYear); setShouldScrollToEmpty(true); } }, [route.params?.regionsToSave]); function extractNumberAndExtension(path: string | null) { if (!path) return null; const slashIndex = path.lastIndexOf('/'); return path.substring(slashIndex + 1); } useEffect(() => { if (editData && editData.trip) { setDescription(editData.trip.description); setRegions( editData.trip.regions.map((region: any) => { const instanceId = `r-${instanceCounterRef.current++}`; const hasYearOnly = region.year_from && !region.day_from; return { ...region, _instanceId: instanceId, id: region.region, flag1: extractNumberAndExtension(region.flag1), flag2: extractNumberAndExtension(region.flag2), visitStartDate: { year: region.year_from || null, month: region.month_from || null, day: region.day_from || null }, visitEndDate: { year: region.year_to || null, month: region.month_to || null, day: region.day_to || null }, quality: region.quality ?? 3, dateMode: hasYearOnly ? 'approx' : 'exact' }; }) ); } }, [editData]); useEffect(() => { setDisabled(!regions?.length); }, [regions]); const formatDateForDisplay = (d?: DateValue | null) => { if (!d || !d.year) return 'Select visit dates'; const m = moment(`${d.year}-${d.month}-${d.day}`, 'YYYY-M-D'); if (!d.day) return m.format('YYYY'); return m.format('D MMM YYYY'); }; const dateValueToISO = (d?: DateValue | null): string | null => { if (!d?.year) return null; if (!d.month || !d.day) return `${d.year}-01-01`; const mm = String(d.month).padStart(2, '0'); const dd = String(d.day).padStart(2, '0'); return `${d.year}-${mm}-${dd}`; }; const parseISOToDateValue = (iso?: string | null): DateValue | null => { if (!iso) return null; const m = moment(iso, 'YYYY-MM-DD'); if (!m.isValid()) return null; return { year: m.year(), month: m.month() + 1, day: m.date() }; }; const validateRegionsDates = (regionsToValidate?: RegionWithDates[] | null) => { const list = regionsToValidate ?? regions; const errors: { [instanceId: string]: string } = {}; if (!list || list.length === 0) { setRegionErrors(errors); return false; } for (let i = 0; i < list.length; i++) { const r = list[i]; const s = r.visitStartDate; const e = r.visitEndDate; const id = r._instanceId ?? `idx-${i}`; if (!isValidDate(s)) { errors[id] = s?.year && s.year >= CURRENT_YEAR ? 'Current or future year requires a full date' : 'Please select visit dates'; continue; } if (!isValidDate(e)) { errors[id] = e?.year && e.year >= CURRENT_YEAR ? 'Current or future year requires a full date' : 'Please select visit dates'; continue; } if (isFullDate(s) && isFullDate(e)) { const sM = moment(`${s!.year}-${s!.month}-${s!.day}`, 'YYYY-M-D'); const eM = moment(`${e!.year}-${e!.month}-${e!.day}`, 'YYYY-M-D'); if (sM.isAfter(eM)) { errors[id] = 'Start date cannot be after end date'; continue; } } else { if ((s?.year ?? 0) > (e?.year ?? 0)) { errors[id] = 'Start year cannot be after end year'; continue; } } } setRegionErrors(errors); return Object.keys(errors).length === 0; }; const openRangeCalendarForRegion = (index: number) => { if (!regions) return; setCalendarVisibleForIndex(index); }; const closeRangeCalendar = ( startDate?: string | null, endDate?: string | null, approxYear?: number | null ) => { const clickedInstanceIndex = calendarVisibleForIndex; setCalendarVisibleForIndex(null); if (clickedInstanceIndex === null || clickedInstanceIndex === undefined) return; if (!startDate && !approxYear) return; const openedInstanceId = regions?.[clickedInstanceIndex]?._instanceId; if (!openedInstanceId) return; let newStartDate: DateValue; let newEndDate: DateValue; let dateMode: CalendarMode; if (approxYear) { newStartDate = { year: approxYear, month: null, day: null }; newEndDate = { year: approxYear, month: null, day: null }; dateMode = 'approx'; } else { const startVal = parseISOToDateValue(startDate); const endVal = parseISOToDateValue(endDate ?? startDate); if (!startVal || !endVal) return; newStartDate = startVal; newEndDate = endVal; dateMode = 'exact'; } const updatedBeforeSort = (regions ?? []).map((r) => r._instanceId === openedInstanceId ? { ...r, visitStartDate: newStartDate, visitEndDate: newEndDate, dateMode } : r ); let sorted: RegionWithDates[]; if (dateMode === 'exact') { const sortKey = (r: RegionWithDates) => dateValueToISO(r.visitStartDate) ?? '9999-12-31'; sorted = [...updatedBeforeSort].sort((a, b) => sortKey(a).localeCompare(sortKey(b))); const newIndex = sorted.findIndex((r) => r._instanceId === openedInstanceId); if (newIndex !== -1) { const next = sorted[newIndex + 1]; if (next && !next.visitStartDate?.year) { sorted[newIndex + 1] = { ...next, visitStartDate: newEndDate, visitEndDate: newEndDate }; } const prev = sorted[newIndex - 1]; if (prev && !prev.visitStartDate?.year) { sorted[newIndex - 1] = { ...prev, visitStartDate: newStartDate, visitEndDate: newStartDate }; } } } else { sorted = updatedBeforeSort; const editedRegionId = regions?.[clickedInstanceIndex]?.id; const propagatedIdSet = new Set(); propagatedIdSet.add(editedRegionId); sorted = sorted.map((r: any) => { if (r._instanceId === openedInstanceId) return r; if (r.visitStartDate?.year) return r; if (propagatedIdSet.has(r.id)) return r; propagatedIdSet.add(r.id); return { ...r, visitStartDate: { year: approxYear, month: null, day: null }, visitEndDate: { year: approxYear, month: null, day: null }, dateMode: 'approx' as CalendarMode }; }); } setRegions(sorted); validateRegionsDates(sorted); setRegionErrors((prev) => { const clone = { ...prev }; delete clone[openedInstanceId]; return clone; }); }; const moveRegionUp = (index: number) => { if (index <= 0 || !regions) return; const r = [...regions]; [r[index - 1], r[index]] = [r[index], r[index - 1]]; setRegions(r); }; const moveRegionDown = (index: number) => { if (!regions || index >= regions.length - 1) return; const r = [...regions]; [r[index + 1], r[index]] = [r[index], r[index + 1]]; setRegions(r); }; const handleDeleteRegion = (index: number) => { if (!regions) return; const updated = [...regions]; updated.splice(index, 1); setRegions(updated); }; const handleDeleteTrip = async () => { setIsWarningModalVisible(false); setPendingDelete(true); }; const computePayloadDates = (regionsList: RegionWithDates[]) => { if (!regionsList || regionsList.length === 0) return { date_from: null, date_to: null }; const starts = regionsList .map((r) => dateValueToISO(r.visitStartDate)) .filter((v): v is string => v !== null); const ends = regionsList .map((r) => dateValueToISO(r.visitEndDate)) .filter((v): v is string => v !== null); if (!starts.length || !ends.length) return { date_from: null, date_to: null }; const toComparableStart = (v: string) => v.length === 4 ? `${v}-01-01` : v; const toComparableEnd = (v: string) => v.length === 4 ? `${v}-12-31` : v; const minStart = starts.reduce((acc, cur) => toComparableStart(cur) < toComparableStart(acc) ? cur : acc ); const maxEnd = ends.reduce((acc, cur) => toComparableEnd(cur) > toComparableEnd(acc) ? cur : acc ); return { date_from: minStart, date_to: maxEnd }; }; const performSaveNewTrip = async () => { if (!regions) return; try { setIsLoading('save'); const regionsData = regions.map((region) => ({ id: region.id, quality: 3, hidden: region.hidden, year_from: region.visitStartDate?.year || null, year_to: region.visitEndDate?.year || null, month_from: region.visitStartDate?.month || null, month_to: region.visitEndDate?.month || null, day_from: region.visitStartDate?.day || null, day_to: region.visitEndDate?.day || null })); const { date_from, date_to } = computePayloadDates(regions); saveNewTrip( { token, date_from, date_to, description, regions: regionsData }, { onSuccess: (res) => { console.log('res', res) if (res && res.result === 'OK') { navigation.popTo(NAVIGATION_PAGES.TRIPS_2025, { saved: true }); } setIsLoading(null); }, onError: (err) => { setIsLoading(null) console.log('err', err) } } ); } catch (err) { console.log('Save trip failed', err); } }; const performUpdateTrip = async () => { if (!regions) return; try { setIsLoading('update'); const regionsData = regions.map((region) => ({ id: region.id, quality: region.quality ?? 3, hidden: region.hidden, year_from: region.visitStartDate?.year || null, year_to: region.visitEndDate?.year || null, month_from: region.visitStartDate?.month || null, month_to: region.visitEndDate?.month || null, day_from: region.visitStartDate?.day || null, day_to: region.visitEndDate?.day || null })); const { date_from, date_to } = computePayloadDates(regions); updateTrip( { token, trip_id: editTripId, date_from, date_to, description, regions: regionsData }, { onSuccess: (res) => { if (res && res.result === 'OK') { navigation.popTo(NAVIGATION_PAGES.TRIPS_2025, { updated: true }); } setIsLoading(null); }, onError: () => setIsLoading(null) } ); } catch (err) { console.log('Update trip failed', err); } }; const handleSaveNewTrip = () => { if (!regions) return; if (!validateRegionsDates(regions)) { scrollToError(regionErrors); return; } const summary = computeTripSummary(regions); if (summary.totalDays === 0) { performSaveNewTrip(); return; } setSummaryData({ ...summary, perRegion: summary.perRegion?.sort((a: any, b: any) => b.days - a.days) || [] }); setPendingAction('save'); summarySheetRef.current?.show(); }; const handleUpdateTrip = () => { if (!regions) return; if (!validateRegionsDates(regions)) { scrollToError(regionErrors); return; } const summary = computeTripSummary(regions); if (summary.totalDays === 0) { performUpdateTrip(); return; } setSummaryData({ ...summary, perRegion: summary.perRegion?.sort((a: any, b: any) => b.days - a.days) || [] }); setPendingAction('update'); summarySheetRef.current?.show(); }; const calendarProps = getCalendarProps(calendarVisibleForIndex); return (
setDescription(text)} value={description} header="Description" height={36} multiline={true} /> Regions {regions && regions.length ? ( {regions.map((region, index) => { const startLabel = formatDateForDisplay(region.visitStartDate); const endLabel = formatDateForDisplay(region.visitEndDate); const datesLabel = region.visitStartDate?.year && region.visitEndDate?.year && dateValueToISO(region.visitStartDate) === dateValueToISO(region.visitEndDate) ? startLabel : region.visitStartDate?.year && region.visitEndDate?.year ? `${startLabel} - ${endLabel}` : 'Select visit dates'; return ( handleDeleteRegion(index)} onSelectDates={() => openRangeCalendarForRegion(index)} datesLabel={datesLabel} onMoveUp={() => moveRegionUp(index)} onMoveDown={() => moveRegionDown(index)} errorMessage={regionErrors[region?._instanceId ?? index]} startLabel={startLabel} endLabel={endLabel} /> ); })} ) : ( No regions at the moment )} navigation.navigate( ...([ NAVIGATION_PAGES.ADD_REGIONS_NEW, { regionsParams: regions, editId: editTripId, defaultYear, allRegions } ] as never) ) } > Add new visit {editTripId ? ( <> setIsWarningModalVisible(true)} disabled={isLoading === 'delete'} > {isLoading === 'delete' ? ( ) : ( Delete trip )} {isLoading === 'update' ? ( ) : ( Save trip )} ) : ( {isLoading === 'save' ? ( ) : ( Save new trip )} )} i !== calendarVisibleForIndex && regions[i]?.visitStartDate?.year) ?.visitStartDate?.year ?? (defaultYear ? Number(defaultYear) : undefined) : undefined } initialMonth={ calendarVisibleForIndex !== null ? regions?.[calendarVisibleForIndex]?.visitStartDate?.month ?? undefined : undefined } withHint={true} /> setIsWarningModalVisible(false)} title="Delete Trip" message="Are you sure you want to delete your trip?" action={handleDeleteTrip} onModalHide={() => { if (pendingDelete) { setPendingDelete(false); setIsLoading('delete'); deleteTrip( { token, trip_id: editTripId }, { onSuccess: (res) => { if (res && res.result === 'OK') { navigation.popTo( NAVIGATION_PAGES.TRIPS_2025, { deleted: true } ); } setIsLoading(null); }, onError: () => { setIsLoading(null); } } ); } }} /> { summarySheetRef.current?.hide(); isClosingRef.current = true; }} onClose={async () => { if (!isClosingRef.current) return; if (pendingAction === 'save') performSaveNewTrip(); if (pendingAction === 'update') performUpdateTrip(); isClosingRef.current = false; }} /> ); }; export default AddNewTripScreen;