import React, { useEffect, useState } from 'react'; import { View, Text, TouchableOpacity, ScrollView, Alert } 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 RangeCalendar from 'src/components/Calendars/RangeCalendar'; 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 } from '@api/trips'; import { styles } from './styles'; import { ActivityIndicator } from 'react-native-paper'; 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; } 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 [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 { mutate: saveNewTrip } = usePostSetNewTripMutation(); const { mutate: updateTrip } = usePostUpdateTripMutation(); const { mutate: deleteTrip } = usePostDeleteTripMutation(); const scrollRef = React.useRef(null); const instanceCounterRef = React.useRef(1); 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; const itemHeight = 160; scrollRef.current?.scrollTo({ y: idx * itemHeight, animated: true }); }; const autoFillAfterAppend = (list: RegionWithDates[]) => { 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 ( r.visitStartDate?.year && r.visitStartDate?.month && r.visitStartDate?.day && r.visitEndDate?.year && r.visitEndDate?.month && r.visitEndDate?.day ) { lastWithDateIndex = i; break; } } if (lastWithDateIndex === null) return updated; const lastDate: DateValue = updated[lastWithDateIndex].visitEndDate as DateValue; for (let i = lastWithDateIndex + 1; i < updated.length; i++) { const r = updated[i]; const hasStart = !!( r.visitStartDate?.year && r.visitStartDate?.month && r.visitStartDate?.day ); const hasEnd = !!(r.visitEndDate?.year && r.visitEndDate?.month && r.visitEndDate?.day); if (!hasStart || !hasEnd) { updated[i] = { ...r, _instanceId: `r-${instanceCounterRef.current++}`, visitStartDate: lastDate, visitEndDate: lastDate }; break; } } return updated; }; useEffect(() => { if (route.params?.regionsToSave) { const filled = autoFillAfterAppend(route.params.regionsToSave); setRegions(filled); } }, [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++}`; 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 } }; }) ); } }, [editData]); useEffect(() => { setDisabled(!regions?.length); }, [regions]); const formatDateForDisplay = (d?: DateValue | null) => { if (!d || !d.year) return 'Select dates'; const m = moment(`${d.year}-${d.month}-${d.day}`, 'YYYY-M-D'); return m.format('D MMM YYYY'); }; const dateValueToISO = (d?: DateValue | null) => { if (!d || !d.year) return null; 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 (!s?.year || !s?.month || !s?.day) { errors[id] = 'Please select visit dates'; continue; } if (!e?.year || !e?.month || !e?.day) { errors[id] = 'Please select visit dates'; continue; } 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; } // if (i > 0) { // const prevEnd = list[i - 1]?.visitEndDate; // if (prevEnd?.year) { // const prevEndM = moment(`${prevEnd.year}-${prevEnd.month}-${prevEnd.day}`, 'YYYY-M-D'); // if (sM.isBefore(prevEndM)) { // errors[i] = 'This region must start before previous'; // continue; // } // } // } } setRegionErrors(errors); return Object.keys(errors).length === 0; }; const isFullDate = (d?: DateValue | null) => { return !!(d?.year && d?.month && d?.day); }; const openRangeCalendarForRegion = (index: number) => { if (!regions) return; setCalendarVisibleForIndex(index); }; const closeRangeCalendar = (startDate?: string | null, endDate?: string | null) => { const clickedInstanceIndex = calendarVisibleForIndex; setCalendarVisibleForIndex(null); if (clickedInstanceIndex === null || clickedInstanceIndex === undefined || !startDate) return; const startVal = parseISOToDateValue(startDate); const endVal = parseISOToDateValue(endDate ?? startDate); if (!startVal || !endVal) return; const openedInstanceId = regions?.[clickedInstanceIndex]?._instanceId; if (!openedInstanceId) { return; } const updatedBeforeSort = (regions ?? []).map((r) => r._instanceId === openedInstanceId ? { ...r, visitStartDate: startVal, visitEndDate: endVal } : r ); const sortKey = (r: RegionWithDates) => { const iso = dateValueToISO(r.visitStartDate); return iso ?? '9999-12-31'; }; const 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 && !isFullDate(next.visitStartDate)) { sorted[newIndex + 1] = { ...next, visitStartDate: endVal, visitEndDate: endVal }; } const prev = sorted[newIndex - 1]; if (prev && !isFullDate(prev.visitStartDate)) { sorted[newIndex - 1] = { ...prev, visitStartDate: startVal, visitEndDate: startVal }; } } setRegions(sorted); validateRegionsDates(sorted); setRegionErrors((prev) => { const clone = { ...prev }; delete clone[clickedInstanceIndex]; return clone; }); }; const moveRegionUp = (index: number) => { if (index <= 0 || !regions) return; const newRegions = [...regions]; [newRegions[index - 1], newRegions[index]] = [newRegions[index], newRegions[index - 1]]; setRegions(newRegions); }; const moveRegionDown = (index: number) => { if (!regions || index >= regions.length - 1) return; const newRegions = [...regions]; [newRegions[index + 1], newRegions[index]] = [newRegions[index], newRegions[index + 1]]; setRegions(newRegions); }; 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) as string); const ends = regionsList.map((r) => dateValueToISO(r.visitEndDate) as string); const minStart = starts.reduce( (acc, cur) => (!acc || cur < acc ? cur : acc), null as string | null ); const maxEnd = ends.reduce( (acc, cur) => (!acc || cur > acc ? cur : acc), null as string | null ); return { date_from: minStart, date_to: maxEnd }; }; const handleSaveNewTrip = () => { if (regions) { if (!validateRegionsDates(regions)) { scrollToError(regionErrors); return; } setIsLoading('save'); const regionsData = regions.map((region) => { return { 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 }; }); if (regionsData.length > 30) { Alert.alert('One trip cannot have more than 30 regions.'); setIsLoading(null); return; } const { date_from, date_to } = computePayloadDates(regions); saveNewTrip( { token, date_from, date_to, description, regions: regionsData }, { onSuccess: (res) => { if (res && res.result === 'OK') { navigation.popTo(...([NAVIGATION_PAGES.TRIPS_2025, { saved: true }] as never)); } setIsLoading(null); }, onError: () => { setIsLoading(null); } } ); } }; const handleUpdateTrip = () => { if (regions) { if (!validateRegionsDates(regions)) { scrollToError(regionErrors); return; } setIsLoading('update'); const regionsData = regions.map((region) => { return { 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 }; }); if (regionsData.length > 30) { Alert.alert('One trip cannot have more than 30 regions.'); setIsLoading(null); return; } 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 }] as never)); } setIsLoading(null); }, onError: () => { setIsLoading(null); } } ); } }; return (
setDescription(text)} value={description} header="Description" height={54} 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 } ] as never) ) } > Add new visit {editTripId ? ( <> setIsWarningModalVisible(true)} disabled={isLoading === 'delete'} > {isLoading === 'delete' ? ( ) : ( Delete trip )} {isLoading === 'update' ? ( ) : ( Save trip )} ) : ( {isLoading === 'save' ? ( ) : ( Save new trip )} )} closeRangeCalendar(startDate, endDate) } initialStartDate={ calendarVisibleForIndex !== null && regions?.[calendarVisibleForIndex]?.visitStartDate && regions[calendarVisibleForIndex].visitStartDate?.day ? `${regions[calendarVisibleForIndex].visitStartDate.year}-${regions[calendarVisibleForIndex].visitStartDate.month}-${regions[calendarVisibleForIndex].visitStartDate.day}` : undefined } initialEndDate={ calendarVisibleForIndex !== null && regions?.[calendarVisibleForIndex]?.visitEndDate && regions[calendarVisibleForIndex].visitEndDate?.day ? `${regions[calendarVisibleForIndex].visitEndDate.year}-${regions[calendarVisibleForIndex].visitEndDate.month}-${regions[calendarVisibleForIndex].visitEndDate.day}` : undefined } /> 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 }] as never) ); } setIsLoading(null); }, onError: () => { setIsLoading(null); } } ); } }} /> ); }; export default AddNewTripScreen;