| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639 |
- 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<string>('');
- const [regions, setRegions] = useState<RegionWithDates[] | null>(null);
- const [disabled, setDisabled] = useState(true);
- const [isLoading, setIsLoading] = useState<string | null>(null);
- const [isWarningModalVisible, setIsWarningModalVisible] = useState(false);
- const [pendingDelete, setPendingDelete] = useState(false);
- const [calendarVisibleForIndex, setCalendarVisibleForIndex] = useState<number | null>(null);
- const [regionErrors, setRegionErrors] = useState<{ [instanceId: string]: string }>({});
- const { mutate: saveNewTrip } = usePostSetNewTripMutation();
- const { mutate: updateTrip } = usePostUpdateTripMutation();
- const { mutate: deleteTrip } = usePostDeleteTripMutation();
- const scrollRef = React.useRef<ScrollView>(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 (
- <PageWrapper style={{ flex: 1 }}>
- <Header label={editTripId ? 'Edit Trip' : 'Add New Trip'} />
- <ScrollView
- ref={scrollRef}
- contentContainerStyle={{ flexGrow: 1, gap: 16 }}
- showsVerticalScrollIndicator={false}
- >
- <Input
- placeholder="Add description and all interesting moments of your trip"
- inputMode={'text'}
- onChange={(text) => setDescription(text)}
- value={description}
- header="Description"
- height={54}
- multiline={true}
- />
- <View style={{ marginBottom: 8 }}>
- <Text style={styles.regionsLabel}>Regions</Text>
- {regions && regions.length ? (
- <View style={styles.regionsContainer}>
- {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 (
- <RegionItem
- key={`${region.id}-${index}`}
- region={region}
- index={index}
- total={regions.length}
- onDelete={() => handleDeleteRegion(index)}
- onSelectDates={() => openRangeCalendarForRegion(index)}
- datesLabel={datesLabel}
- onMoveUp={() => moveRegionUp(index)}
- onMoveDown={() => moveRegionDown(index)}
- errorMessage={regionErrors[region?._instanceId ?? index]}
- startLabel={startLabel}
- endLabel={endLabel}
- />
- );
- })}
- </View>
- ) : (
- <Text style={styles.noRegiosText}>No regions at the moment</Text>
- )}
- </View>
- </ScrollView>
- <View style={{ flexDirection: 'column', gap: 6, backgroundColor: 'transparent' }}>
- <TouchableOpacity
- style={[styles.addRegionBtn]}
- onPress={() =>
- navigation.navigate(
- ...([
- NAVIGATION_PAGES.ADD_REGIONS_NEW,
- { regionsParams: regions, editId: editTripId }
- ] as never)
- )
- }
- >
- <Text style={styles.addRegionBtntext}>Add new visit</Text>
- </TouchableOpacity>
- <View style={styles.tabContainer}>
- {editTripId ? (
- <>
- <TouchableOpacity
- style={[styles.tabStyle, styles.deleteTab]}
- onPress={() => setIsWarningModalVisible(true)}
- disabled={isLoading === 'delete'}
- >
- {isLoading === 'delete' ? (
- <ActivityIndicator size={18} color={Colors.RED} />
- ) : (
- <Text style={[styles.tabText, styles.deleteTabText]}>Delete trip</Text>
- )}
- </TouchableOpacity>
- <TouchableOpacity
- style={[
- styles.tabStyle,
- styles.addNewTab,
- disabled && { backgroundColor: Colors.LIGHT_GRAY, borderColor: Colors.LIGHT_GRAY }
- ]}
- onPress={handleUpdateTrip}
- disabled={disabled || isLoading === 'update'}
- >
- {isLoading === 'update' ? (
- <ActivityIndicator size={18} color={Colors.WHITE} />
- ) : (
- <Text style={[styles.tabText, styles.addNewTabText]}>Save trip</Text>
- )}
- </TouchableOpacity>
- </>
- ) : (
- <TouchableOpacity
- style={[
- styles.tabStyle,
- styles.addNewTab,
- disabled && { backgroundColor: Colors.LIGHT_GRAY, borderColor: Colors.LIGHT_GRAY },
- { paddingVertical: 12 }
- ]}
- onPress={handleSaveNewTrip}
- disabled={disabled || isLoading === 'save'}
- >
- {isLoading === 'save' ? (
- <ActivityIndicator size={18} color={Colors.WHITE} />
- ) : (
- <Text style={[styles.tabText, styles.addNewTabText]}>Save new trip</Text>
- )}
- </TouchableOpacity>
- )}
- </View>
- </View>
- <RangeCalendar
- isModalVisible={calendarVisibleForIndex !== null}
- allowRangeSelection={true}
- closeModal={(startDate?: string | null, endDate?: string | null) =>
- 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
- }
- />
- <WarningModal
- type={'delete'}
- isVisible={isWarningModalVisible}
- onClose={() => 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);
- }
- }
- );
- }
- }}
- />
- </PageWrapper>
- );
- };
- export default AddNewTripScreen;
|