Ver Fonte

only a year option for trips

Viktoriia há 1 semana atrás
pai
commit
102ef35295

+ 468 - 0
src/components/Calendars/RangeCalendar/RangeCalendarWithTabs.tsx

@@ -0,0 +1,468 @@
+import React, { useEffect, useRef, useState } from 'react';
+import {
+  View,
+  Text,
+  TouchableOpacity,
+  StyleSheet,
+  Animated,
+  LayoutChangeEvent
+} from 'react-native';
+import { Picker as WheelPicker } from 'react-native-wheel-pick';
+import { Modal } from '../../Modal';
+import DateTimePicker, { DateType } from 'react-native-ui-datepicker';
+import dayjs from 'dayjs';
+import 'dayjs/locale/en';
+import calendar from 'dayjs/plugin/calendar';
+import updateLocale from 'dayjs/plugin/updateLocale';
+import localeData from 'dayjs/plugin/localeData';
+import customParseFormat from 'dayjs/plugin/customParseFormat';
+
+dayjs.locale('en');
+dayjs.extend(calendar);
+dayjs.extend(updateLocale);
+dayjs.extend(localeData);
+dayjs.extend(customParseFormat);
+
+import { styles } from './style';
+import { Colors } from '../../../theme';
+import { Button } from 'src/components/Button';
+import { ButtonVariants } from 'src/types/components';
+import Navigation from './Navigation';
+
+export type CalendarMode = 'exact' | 'approx';
+
+const CURRENT_YEAR = dayjs().year();
+
+const YEAR_STRINGS: string[] = Array.from(
+  { length: 100 },
+  (_, i) => String(CURRENT_YEAR - 1 - i)
+);
+
+const FIXED_CONTENT_HEIGHT = 380;
+
+function TabSwitcher({
+  activeTab,
+  onTabChange
+}: {
+  activeTab: CalendarMode;
+  onTabChange: (tab: CalendarMode) => void;
+}) {
+  const slideAnim = useRef(new Animated.Value(activeTab === 'exact' ? 0 : 1)).current;
+  const [pillWidth, setPillWidth] = useState(0);
+
+  useEffect(() => {
+    Animated.spring(slideAnim, {
+      toValue: activeTab === 'exact' ? 0 : 1,
+      useNativeDriver: true,
+      tension: 68,
+      friction: 12
+    }).start();
+  }, [activeTab]);
+
+  const handleLayout = (e: LayoutChangeEvent) => {
+    setPillWidth((e.nativeEvent.layout.width - 8) / 2);
+  };
+
+  const translateX = slideAnim.interpolate({
+    inputRange: [0, 1],
+    outputRange: [0, pillWidth]
+  });
+
+  return (
+    <View style={tabStyles.wrapper} onLayout={handleLayout}>
+      <Animated.View
+        style={[tabStyles.pill, { width: pillWidth, transform: [{ translateX }] }]}
+      />
+      {(['exact', 'approx'] as CalendarMode[]).map((tab) => (
+        <TouchableOpacity
+          key={tab}
+          style={tabStyles.tab}
+          onPress={() => onTabChange(tab)}
+          activeOpacity={0.85}
+        >
+          <Text style={[tabStyles.label, activeTab === tab && tabStyles.labelActive]}>
+            {tab === 'exact' ? 'Exact dates' : 'Year'}
+          </Text>
+        </TouchableOpacity>
+      ))}
+    </View>
+  );
+}
+
+const tabStyles = StyleSheet.create({
+  wrapper: {
+    flexDirection: 'row',
+    backgroundColor: Colors.FILL_LIGHT,
+    borderRadius: 50,
+    padding: 4,
+    marginHorizontal: 16,
+    marginBottom: 12,
+    position: 'relative',
+    overflow: 'hidden',
+  },
+  pill: {
+    position: 'absolute',
+    top: 4,
+    left: 4,
+    bottom: 4,
+    borderRadius: 50,
+    backgroundColor: Colors.DARK_BLUE,
+    shadowColor: '#000',
+    shadowOffset: { width: 0, height: 2 },
+    shadowOpacity: 0.14,
+    shadowRadius: 4,
+    elevation: 4
+  },
+  tab: {
+    flex: 1,
+    paddingVertical: 8,
+    alignItems: 'center',
+    zIndex: 1
+  },
+  label: {
+    fontSize: 14,
+    fontWeight: '500',
+    color: Colors.TEXT_GRAY
+  },
+  labelActive: {
+    color: Colors.WHITE,
+    fontWeight: '600'
+  }
+});
+
+function YearWheelPicker({
+  selectedYear,
+  onYearChange
+}: {
+  selectedYear: number;
+  onYearChange: (year: number) => void;
+}) {
+  return (
+    <View style={wheelStyles.container}>
+      <WheelPicker
+        style={wheelStyles.wheel}
+        pickerData={YEAR_STRINGS}
+        selectedValue={String(selectedYear)}
+        onValueChange={(value: string) => onYearChange(Number(value))}
+        textColor={Colors.TEXT_GRAY}
+        textSize={22}
+        selectTextColor={Colors.DARK_BLUE}
+        isAtmospheric={true}
+        isCyclic={false}
+        isShowSelectLine={true}
+        selectLineColor={Colors.ORANGE}
+        selectLineSize={1}
+        itemStyle={{
+          fontSize: 22,
+          fontFamily: 'montserrat-600',
+          height: 380
+        }}
+        selectedItemTextStyle={{
+          fontSize: 30,
+          fontFamily: 'montserrat-700',
+          color: Colors.DARK_BLUE
+        }}
+      />
+    </View>
+  );
+}
+
+const wheelStyles = StyleSheet.create({
+  container: {
+    flex: 1,
+    alignItems: 'center',
+  },
+  wheel: {
+    width: '100%',
+    backgroundColor: 'transparent'
+  }
+});
+
+export default function RangeCalendarWithTabs({
+  isModalVisible,
+  closeModal,
+  allowRangeSelection = true,
+  disableFutureDates = false,
+  disablePastDates = false,
+  highlightedDates,
+  selectedDate,
+  minDate: externalMinDate,
+  maxDate: externalMaxDate,
+  initialStartDate,
+  initialEndDate,
+  initialYear,
+  initialMonth,
+  withHint = false,
+  defaultMode = 'exact',
+  initialApproxYear
+}: {
+  isModalVisible: boolean;
+  closeModal: (
+    startDate?: string | null,
+    endDate?: string | null,
+    approxYear?: number | null
+  ) => void;
+  allowRangeSelection?: boolean;
+  disableFutureDates?: boolean;
+  disablePastDates?: boolean;
+  highlightedDates?: string[];
+  selectedDate?: string;
+  minDate?: string | Date | dayjs.Dayjs;
+  maxDate?: string | Date | dayjs.Dayjs;
+  initialStartDate?: string | null;
+  initialEndDate?: string | null;
+  initialYear?: number;
+  initialMonth?: number;
+  withHint?: boolean;
+  defaultMode?: CalendarMode;
+  initialApproxYear?: number | null;
+}) {
+  const fallbackYear = CURRENT_YEAR - 1;
+
+  const [activeTab, setActiveTab] = useState<CalendarMode>(defaultMode);
+  const [selectedYear, setSelectedYear] = useState<number>(initialApproxYear ?? fallbackYear);
+
+  const [selectedStartDate, setSelectedStartDate] = useState<string | null>(null);
+  const [selectedEndDate, setSelectedEndDate] = useState<string | null>(null);
+  const [singleDate, setSingleDate] = useState<DateType>(undefined);
+  const [startDate, setStartDate] = useState<DateType>(undefined);
+  const [endDate, setEndDate] = useState<DateType>(undefined);
+  const [currentMonth, setCurrentMonth] = useState<number | undefined>(undefined);
+  const [currentYear, setCurrentYear] = useState<number | undefined>(undefined);
+
+  const computedMinDate = externalMinDate
+    ? dayjs(externalMinDate)
+    : highlightedDates?.length
+      ? dayjs(highlightedDates[0])
+      : undefined;
+
+  const computedMaxDate = externalMaxDate
+    ? dayjs(externalMaxDate)
+    : highlightedDates?.length
+      ? dayjs(highlightedDates[highlightedDates.length - 1])
+      : undefined;
+
+  useEffect(() => {
+    if (!isModalVisible) return;
+
+    setActiveTab(defaultMode);
+
+    setSelectedYear(
+      initialApproxYear && YEAR_STRINGS.includes(String(initialApproxYear))
+        ? initialApproxYear
+        : fallbackYear
+    );
+
+    if (allowRangeSelection) {
+      if (initialStartDate) {
+        const s = dayjs(initialStartDate);
+        setSelectedStartDate(initialStartDate);
+        setStartDate(s);
+        setCurrentMonth(s.month());
+        setCurrentYear(s.year());
+      } else {
+        if (typeof initialYear === 'number' && initialYear !== 1) setCurrentYear(initialYear);
+        if (typeof initialMonth === 'number') setCurrentMonth(initialMonth - 1);
+      }
+      if (initialEndDate) {
+        setSelectedEndDate(initialEndDate);
+        setEndDate(dayjs(initialEndDate));
+      }
+    } else if (selectedDate) {
+      const d = dayjs(selectedDate);
+      setSelectedStartDate(selectedDate);
+      setSingleDate(d);
+      setCurrentMonth(d.month());
+      setCurrentYear(d.year());
+    }
+  }, [
+    isModalVisible,
+    defaultMode,
+    initialApproxYear,
+    initialStartDate,
+    initialEndDate,
+    selectedDate,
+    allowRangeSelection,
+    initialYear,
+    initialMonth
+  ]);
+
+  const getDisabledDates = (date: DateType) => {
+    const ds = dayjs(date).format('YYYY-MM-DD');
+    if (disableFutureDates && dayjs(date).isAfter(dayjs(), 'day')) return true;
+    if (disablePastDates && dayjs(date).isBefore(dayjs(), 'day')) return true;
+    if (highlightedDates?.length) return !highlightedDates.includes(ds);
+    return false;
+  };
+
+  const handleDateChange = (params: any) => {
+    if (allowRangeSelection) {
+      setStartDate(params.startDate);
+      setEndDate(params.endDate);
+      if (params.startDate) setSelectedStartDate(dayjs(params.startDate).format('YYYY-MM-DD'));
+      setSelectedEndDate(params.endDate ? dayjs(params.endDate).format('YYYY-MM-DD') : null);
+    } else {
+      setSingleDate(params.date);
+      setSelectedStartDate(dayjs(params.date).format('YYYY-MM-DD'));
+      setSelectedEndDate(null);
+    }
+  };
+
+  const clearAll = () => {
+    setSelectedStartDate(null);
+    setSelectedEndDate(null);
+    setStartDate(undefined);
+    setEndDate(undefined);
+    setSingleDate(undefined);
+    setSelectedYear(fallbackYear);
+    setTimeout(() => {
+      setCurrentMonth(undefined);
+      setCurrentYear(undefined);
+    }, 200);
+  };
+
+  const resetSelections = () => {
+    closeModal();
+    clearAll();
+  };
+
+  const handleClose = () => {
+    if (activeTab === 'approx') {
+      closeModal(null, null, selectedYear);
+    } else {
+      closeModal(selectedStartDate, selectedEndDate, null);
+    }
+    clearAll();
+  };
+
+  const isDoneEnabled = activeTab === 'approx' ? !!selectedYear : !!selectedStartDate;
+
+  return (
+    <Modal
+      visibleInPercent={'auto'}
+      visible={isModalVisible}
+      onRequestClose={resetSelections}
+      headerTitle={allowRangeSelection ? 'Select Dates' : 'Select Date'}
+    >
+      <View style={styles.modalContent}>
+        <TabSwitcher activeTab={activeTab} onTabChange={setActiveTab} />
+
+        <View style={{ height: FIXED_CONTENT_HEIGHT }}>
+          <View
+            style={[StyleSheet.absoluteFill, { opacity: activeTab === 'exact' ? 1 : 0 }]}
+            pointerEvents={activeTab === 'exact' ? 'auto' : 'none'}
+          >
+            {startDate && endDate && withHint && (
+              <Text style={{
+                color: Colors.TEXT_GRAY,
+                fontSize: 12,
+                textAlign: 'center',
+                fontWeight: '500',
+                marginBottom: 8
+              }}>
+                Double-tap to mark the first day of your trip.
+              </Text>
+            )}
+            <DateTimePicker
+              key={`${currentMonth}-${currentYear}`}
+              mode={allowRangeSelection ? 'range' : 'single'}
+              date={singleDate}
+              startDate={startDate}
+              endDate={endDate}
+              month={currentMonth}
+              year={currentYear}
+              onChange={handleDateChange}
+              minDate={computedMinDate}
+              maxDate={computedMaxDate}
+              disabledDates={getDisabledDates}
+              firstDayOfWeek={1}
+              timePicker={false}
+              showOutsideDays={true}
+              weekdaysFormat={'short'}
+              components={{
+                IconPrev: <Navigation direction="prev" />,
+                IconNext: <Navigation direction="next" />
+              }}
+              styles={{
+                header: { paddingBottom: 10 },
+                month_selector_label: { color: Colors.DARK_BLUE, fontWeight: 'bold', fontSize: 15 },
+                year_selector_label: { color: Colors.DARK_BLUE, fontWeight: 'bold', fontSize: 15 },
+                button_prev: {},
+                button_next: {},
+                weekday_label: { color: Colors.DARK_BLUE, fontWeight: 'bold', fontSize: 12 },
+                days: {},
+                day_cell: {},
+                day: {},
+                day_label: { color: Colors.DARK_BLUE, fontSize: 14, fontWeight: 'normal' },
+                today: {
+                  borderWidth: 1,
+                  borderRadius: 23,
+                  width: 46,
+                  height: 46,
+                  maxHeight: 46,
+                  borderColor: Colors.ORANGE
+                },
+                today_label: { color: Colors.DARK_BLUE, fontWeight: '500' },
+                selected: {
+                  backgroundColor: Colors.ORANGE,
+                  borderRadius: 23,
+                  width: 46,
+                  height: 46,
+                  maxHeight: 46,
+                  marginVertical: 'auto',
+                  alignItems: 'center',
+                  justifyContent: 'center'
+                },
+                selected_label: { color: 'white', fontWeight: '500' },
+                range_start: {
+                  backgroundColor: Colors.ORANGE,
+                  borderRadius: 23,
+                  width: 46,
+                  height: 46,
+                  alignItems: 'center',
+                  justifyContent: 'center'
+                },
+                range_start_label: { color: 'white' },
+                range_end: {
+                  backgroundColor: Colors.ORANGE,
+                  borderRadius: 23,
+                  width: 46,
+                  height: 46,
+                  alignItems: 'center',
+                  justifyContent: 'center'
+                },
+                range_end_label: { color: 'white' },
+                range_middle_label: { color: Colors.DARK_BLUE, fontWeight: '500' },
+                range_fill: { backgroundColor: Colors.ORANGE, opacity: 0.2 },
+                disabled: { opacity: 0.3 },
+                disabled_label: { color: Colors.TEXT_GRAY },
+                outside_label: { color: Colors.LIGHT_GRAY }
+              }}
+            />
+          </View>
+
+          <View
+            style={[StyleSheet.absoluteFill, { opacity: activeTab === 'approx' ? 1 : 0 }]}
+            pointerEvents={activeTab === 'approx' ? 'auto' : 'none'}
+          >
+            <YearWheelPicker
+              selectedYear={selectedYear}
+              onYearChange={setSelectedYear}
+            />
+          </View>
+
+        </View>
+      </View>
+
+      <View style={styles.modalFooter}>
+        <Button
+          children="Done"
+          onPress={handleClose}
+          disabled={!isDoneEnabled}
+          variant={!isDoneEnabled ? ButtonVariants.OPACITY : ButtonVariants.FILL}
+          containerStyles={{ borderWidth: 0 }}
+        />
+      </View>
+    </Modal>
+  );
+}

+ 205 - 252
src/screens/InAppScreens/TravelsScreen/AddNewTrip2025Screen/index.tsx

@@ -1,11 +1,11 @@
 import React, { useEffect, useState } from 'react';
-import { View, Text, TouchableOpacity, ScrollView, Alert } from 'react-native';
+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 RangeCalendar from 'src/components/Calendars/RangeCalendar';
+import RangeCalendarWithTabs, { CalendarMode } from 'src/components/Calendars/RangeCalendar/RangeCalendarWithTabs';
 
 import { StoreType, storage } from 'src/storage';
 import { Colors } from 'src/theme';
@@ -39,33 +39,39 @@ interface RegionWithDates extends RegionAddData {
   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 [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 [shouldScrollToEmpty, setShouldScrollToEmpty] = useState(false);
 
   const summarySheetRef = React.useRef<ActionSheetRef>(null);
-
-  const [summaryData, setSummaryData] = useState({
-    totalDays: 0,
-    perRegion: []
-  });
-
+  const [summaryData, setSummaryData] = useState({ totalDays: 0, perRegion: [] });
   const [pendingAction, setPendingAction] = useState<'save' | 'update' | null>(null);
   const isClosingRef = React.useRef<boolean>(null);
 
@@ -76,31 +82,38 @@ const AddNewTripScreen = ({ route }: { route: any }) => {
   const scrollRef = React.useRef<ScrollView>(null);
   const instanceCounterRef = React.useRef(1);
 
+  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;
-    const itemHeight = 160;
-    scrollRef.current?.scrollTo({ y: idx * itemHeight, animated: true });
+    scrollRef.current?.scrollTo({ y: idx * 160, animated: true });
   };
 
   const scrollToEmpty = () => {
     if (!regions?.length) return;
-
-    const idx = regions.findIndex((r) => {
-      const startOK = isFullDate(r.visitStartDate);
-      const endOK = isFullDate(r.visitEndDate);
-      return !(startOK && endOK);
-    });
-
+    const idx = regions.findIndex(
+      (r) => !isValidDate(r.visitStartDate) || !isValidDate(r.visitEndDate)
+    );
     if (idx === -1) return;
-
-    const itemHeight = 160;
-    scrollRef.current?.scrollTo({
-      y: idx * itemHeight,
-      animated: true
-    });
+    scrollRef.current?.scrollTo({ y: idx * 160, animated: true });
   };
 
   const computeTripSummary = (list: RegionWithDates[]) => {
@@ -112,7 +125,6 @@ const AddNewTripScreen = ({ route }: { route: any }) => {
 
       if (isTransit) {
         const key = `${r.id}_transit`;
-
         if (!perRegionMap[key]) {
           perRegionMap[key] = {
             id: r.id,
@@ -123,7 +135,6 @@ const AddNewTripScreen = ({ route }: { route: any }) => {
             flag2: r.flag2 ?? null
           };
         }
-
         perRegionMap[key].days += 1;
         continue;
       }
@@ -135,7 +146,6 @@ const AddNewTripScreen = ({ route }: { route: any }) => {
 
       let cursor = moment(startISO);
       const end = moment(endISO);
-
       let regionDays = 0;
 
       while (cursor.isSameOrBefore(end)) {
@@ -145,22 +155,12 @@ const AddNewTripScreen = ({ route }: { route: any }) => {
       }
 
       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] = { 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)
-    };
+    return { totalDays: daySet.size, perRegion: Object.values(perRegionMap) };
   };
 
   const normalizeRegion = (r: RegionWithDates): RegionWithDates => ({
@@ -172,21 +172,13 @@ const AddNewTripScreen = ({ route }: { route: any }) => {
 
   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 (
-        r.visitStartDate?.year &&
-        r.visitStartDate?.month &&
-        r.visitStartDate?.day &&
-        r.visitEndDate?.year &&
-        r.visitEndDate?.month &&
-        r.visitEndDate?.day
-      ) {
+      if (isFullDate(r.visitStartDate) && isFullDate(r.visitEndDate)) {
         lastWithDateIndex = i;
         break;
       }
@@ -198,11 +190,7 @@ const AddNewTripScreen = ({ route }: { route: any }) => {
 
     for (let i = lastWithDateIndex + 1; i < updated.length; i++) {
       const r = updated[i];
-
-      const hasStart = isFullDate(r.visitStartDate);
-      const hasEnd = isFullDate(r.visitEndDate);
-
-      if (!hasStart || !hasEnd) {
+      if (!isFullDate(r.visitStartDate) || !isFullDate(r.visitEndDate)) {
         updated[i] = {
           ...r,
           _instanceId: `r-${instanceCounterRef.current++}`,
@@ -218,7 +206,6 @@ const AddNewTripScreen = ({ route }: { route: any }) => {
 
   useEffect(() => {
     if (!shouldScrollToEmpty || !regions) return;
-
     requestAnimationFrame(() => {
       scrollToEmpty();
       setShouldScrollToEmpty(false);
@@ -231,7 +218,6 @@ const AddNewTripScreen = ({ route }: { route: any }) => {
       const oldCount = oldRegions.length;
       const normalized = route.params.regionsToSave.map(normalizeRegion);
       const filled = autoFillAfterAppend(normalized, oldCount);
-
       setRegions(filled);
       setShouldScrollToEmpty(true);
     }
@@ -249,6 +235,7 @@ const AddNewTripScreen = ({ route }: { route: any }) => {
       setRegions(
         editData.trip.regions.map((region: any) => {
           const instanceId = `r-${instanceCounterRef.current++}`;
+          const hasYearOnly = region.year_from && !region.day_from;
           return {
             ...region,
             _instanceId: instanceId,
@@ -265,7 +252,8 @@ const AddNewTripScreen = ({ route }: { route: any }) => {
               month: region.month_to || null,
               day: region.day_to || null
             },
-            quality: region.quality ?? 3
+            quality: region.quality ?? 3,
+            dateMode: hasYearOnly ? 'approx' : 'exact'
           };
         })
       );
@@ -282,8 +270,9 @@ const AddNewTripScreen = ({ route }: { route: any }) => {
     return !d.month ? m.format('YYYY') : !d.day ? m.format('MMM YYYY') : m.format('D MMM YYYY');
   };
 
-  const dateValueToISO = (d?: DateValue | null) => {
-    if (!d || !d.year) return null;
+  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}`;
@@ -311,120 +300,122 @@ const AddNewTripScreen = ({ route }: { route: any }) => {
       const e = r.visitEndDate;
       const id = r._instanceId ?? `idx-${i}`;
 
-      if (!s?.year || !s?.month || !s?.day) {
-        errors[id] = 'Please select visit dates';
+      if (!isValidDate(s)) {
+        errors[id] = s?.year === CURRENT_YEAR
+          ? 'Current year requires a full date'
+          : 'Please select visit dates';
         continue;
       }
-      if (!e?.year || !e?.month || !e?.day) {
-        errors[id] = 'Please select visit dates';
+      if (!isValidDate(e)) {
+        errors[id] = e?.year === CURRENT_YEAR
+          ? 'Current year requires a full date'
+          : '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 (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;
+        }
       }
-
-      // 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 closeRangeCalendar = (
+    startDate?: string | null,
+    endDate?: string | null,
+    approxYear?: number | 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;
+    if (clickedInstanceIndex === null || clickedInstanceIndex === undefined) return;
+    if (!startDate && !approxYear) return;
 
     const openedInstanceId = regions?.[clickedInstanceIndex]?._instanceId;
-    if (!openedInstanceId) {
-      return;
+    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: startVal, visitEndDate: endVal }
+        ? { ...r, visitStartDate: newStartDate, visitEndDate: newEndDate, dateMode }
         : 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
-        };
+    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 && !isFullDate(next.visitStartDate)) {
+          sorted[newIndex + 1] = { ...next, visitStartDate: newEndDate, visitEndDate: newEndDate };
+        }
+        const prev = sorted[newIndex - 1];
+        if (prev && !isFullDate(prev.visitStartDate)) {
+          sorted[newIndex - 1] = { ...prev, visitStartDate: newStartDate, visitEndDate: newStartDate };
+        }
       }
+    } else {
+      sorted = updatedBeforeSort;
     }
 
     setRegions(sorted);
-
     validateRegionsDates(sorted);
-
     setRegionErrors((prev) => {
       const clone = { ...prev };
-      delete clone[clickedInstanceIndex];
+      delete clone[openedInstanceId];
       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 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 newRegions = [...regions];
-    [newRegions[index + 1], newRegions[index]] = [newRegions[index], newRegions[index + 1]];
-    setRegions(newRegions);
+    const r = [...regions];
+    [r[index + 1], r[index]] = [r[index], r[index + 1]];
+    setRegions(r);
   };
 
   const handleDeleteRegion = (index: number) => {
@@ -442,16 +433,25 @@ const AddNewTripScreen = ({ route }: { route: any }) => {
   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 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) => (!acc || cur < acc ? cur : acc),
-      null as string | null
+    const minStart = starts.reduce((acc, cur) =>
+      toComparableStart(cur) < toComparableStart(acc) ? cur : acc
     );
-    const maxEnd = ends.reduce(
-      (acc, cur) => (!acc || cur > acc ? cur : acc),
-      null as string | null
+    const maxEnd = ends.reduce((acc, cur) =>
+      toComparableEnd(cur) > toComparableEnd(acc) ? cur : acc
     );
 
     return { date_from: minStart, date_to: maxEnd };
@@ -462,45 +462,32 @@ const AddNewTripScreen = ({ route }: { route: any }) => {
 
     try {
       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 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
-        },
+        { 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 }] as never));
             }
             setIsLoading(null);
           },
-          onError: () => {
-            setIsLoading(null);
+          onError: (err) => {
+            setIsLoading(null)
+            console.log('err', err)
           }
         }
       );
@@ -511,40 +498,22 @@ const AddNewTripScreen = ({ route }: { route: any }) => {
 
   const performUpdateTrip = async () => {
     if (!regions) return;
-
     try {
       setIsLoading('update');
-      const regionsData = regions.map((region) => {
-        return {
-          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
-        };
-      });
-
-      // if (regionsData.length > 30) {
-      //   Alert.alert('One trip cannot have more than 30 regions.');
-      //   setIsLoading(null);
-      //   return;
-      // }
-
+      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
-        },
+        { token, trip_id: editTripId, date_from, date_to, description, regions: regionsData },
         {
           onSuccess: (res) => {
             if (res && res.result === 'OK') {
@@ -552,9 +521,7 @@ const AddNewTripScreen = ({ route }: { route: any }) => {
             }
             setIsLoading(null);
           },
-          onError: () => {
-            setIsLoading(null);
-          }
+          onError: () => setIsLoading(null)
         }
       );
     } catch (err) {
@@ -563,49 +530,40 @@ const AddNewTripScreen = ({ route }: { route: any }) => {
   };
 
   const handleSaveNewTrip = () => {
-    if (regions) {
-      if (!validateRegionsDates(regions)) {
-        scrollToError(regionErrors);
-        return;
-      }
-
-      const summary = computeTripSummary(regions);
-      if (summary) {
-        setSummaryData({
-          ...summary,
-          perRegion: summary.perRegion?.sort((a: any, b: any) => b.days - a.days) || []
-        });
-      } else {
-        setSummaryData({ totalDays: 0, perRegion: [] });
-      }
-      setPendingAction('save');
-
-      summarySheetRef.current?.show();
+    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) {
-      if (!validateRegionsDates(regions)) {
-        scrollToError(regionErrors);
-        return;
-      }
-
-      const summary = computeTripSummary(regions);
-      if (summary) {
-        setSummaryData({
-          ...summary,
-          perRegion: summary.perRegion?.sort((a: any, b: any) => b.days - a.days) || []
-        });
-      } else {
-        setSummaryData({ totalDays: 0, perRegion: [] });
-      }
-      setPendingAction('update');
-
-      summarySheetRef.current?.show();
+    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 (
     <PageWrapper style={{ flex: 1 }}>
       <Header label={editTripId ? 'Edit Trip' : 'Add New Trip'} />
@@ -632,9 +590,8 @@ const AddNewTripScreen = ({ route }: { route: any }) => {
                 const startLabel = formatDateForDisplay(region.visitStartDate);
                 const endLabel = formatDateForDisplay(region.visitEndDate);
                 const datesLabel =
-                  region.visitStartDate?.year &&
-                  region.visitEndDate?.year &&
-                  dateValueToISO(region.visitStartDate) === dateValueToISO(region.visitEndDate)
+                  region.visitStartDate?.year && region.visitEndDate?.year &&
+                    dateValueToISO(region.visitStartDate) === dateValueToISO(region.visitEndDate)
                     ? startLabel
                     : region.visitStartDate?.year && region.visitEndDate?.year
                       ? `${startLabel} - ${endLabel}`
@@ -729,36 +686,32 @@ const AddNewTripScreen = ({ route }: { route: any }) => {
         </View>
       </View>
 
-      <RangeCalendar
+      <RangeCalendarWithTabs
         isModalVisible={calendarVisibleForIndex !== null}
         allowRangeSelection={true}
-        closeModal={(startDate?: string | null, endDate?: string | null) =>
-          closeRangeCalendar(startDate, endDate)
-        }
+        closeModal={closeRangeCalendar}
+        defaultMode={calendarProps.defaultMode}
+        initialApproxYear={calendarProps.initialApproxYear}
         initialStartDate={
           calendarVisibleForIndex !== null &&
-          regions?.[calendarVisibleForIndex]?.visitStartDate &&
-          regions[calendarVisibleForIndex].visitStartDate?.day
-            ? `${regions[calendarVisibleForIndex].visitStartDate.year}-${regions[calendarVisibleForIndex].visitStartDate.month}-${regions[calendarVisibleForIndex].visitStartDate.day}`
+            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}`
+            regions?.[calendarVisibleForIndex]?.visitEndDate?.day
+            ? `${regions[calendarVisibleForIndex].visitEndDate!.year}-${regions[calendarVisibleForIndex].visitEndDate!.month}-${regions[calendarVisibleForIndex].visitEndDate!.day}`
             : undefined
         }
         initialYear={
-          calendarVisibleForIndex !== null &&
-          regions?.[calendarVisibleForIndex]?.visitStartDate?.year
-            ? regions[calendarVisibleForIndex].visitStartDate.year
+          calendarVisibleForIndex !== null
+            ? regions?.[calendarVisibleForIndex]?.visitStartDate?.year ?? undefined
             : undefined
         }
         initialMonth={
-          calendarVisibleForIndex !== null &&
-          regions?.[calendarVisibleForIndex]?.visitStartDate?.month
-            ? regions[calendarVisibleForIndex].visitStartDate.month
+          calendarVisibleForIndex !== null
+            ? regions?.[calendarVisibleForIndex]?.visitStartDate?.month ?? undefined
             : undefined
         }
         withHint={true}