|
|
@@ -28,6 +28,7 @@ interface DateValue {
|
|
|
}
|
|
|
|
|
|
interface RegionWithDates extends RegionAddData {
|
|
|
+ _instanceId?: string;
|
|
|
visitStartDate?: DateValue | null;
|
|
|
visitEndDate?: DateValue | null;
|
|
|
year_from?: number;
|
|
|
@@ -52,15 +53,25 @@ const AddNewTripScreen = ({ route }: { route: any }) => {
|
|
|
const [pendingDelete, setPendingDelete] = useState(false);
|
|
|
|
|
|
const [calendarVisibleForIndex, setCalendarVisibleForIndex] = useState<number | null>(null);
|
|
|
- const [calendarMinDate, setCalendarMinDate] = useState<string | undefined>(undefined);
|
|
|
- const [calendarMaxDate, setCalendarMaxDate] = useState<string | undefined>(undefined);
|
|
|
|
|
|
- const [regionErrors, setRegionErrors] = useState<{ [index: number]: string }>({});
|
|
|
+ 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;
|
|
|
|
|
|
@@ -69,6 +80,7 @@ const AddNewTripScreen = ({ route }: { route: any }) => {
|
|
|
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 &&
|
|
|
@@ -98,6 +110,7 @@ const AddNewTripScreen = ({ route }: { route: any }) => {
|
|
|
if (!hasStart || !hasEnd) {
|
|
|
updated[i] = {
|
|
|
...r,
|
|
|
+ _instanceId: `r-${instanceCounterRef.current++}`,
|
|
|
visitStartDate: lastDate,
|
|
|
visitEndDate: lastDate
|
|
|
};
|
|
|
@@ -127,8 +140,10 @@ const AddNewTripScreen = ({ route }: { route: any }) => {
|
|
|
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),
|
|
|
@@ -174,7 +189,7 @@ const AddNewTripScreen = ({ route }: { route: any }) => {
|
|
|
|
|
|
const validateRegionsDates = (regionsToValidate?: RegionWithDates[] | null) => {
|
|
|
const list = regionsToValidate ?? regions;
|
|
|
- const errors: { [index: number]: string } = {};
|
|
|
+ const errors: { [instanceId: string]: string } = {};
|
|
|
|
|
|
if (!list || list.length === 0) {
|
|
|
setRegionErrors(errors);
|
|
|
@@ -185,13 +200,14 @@ const AddNewTripScreen = ({ route }: { route: any }) => {
|
|
|
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[i] = 'Please select visit dates';
|
|
|
+ errors[id] = 'Please select visit dates';
|
|
|
continue;
|
|
|
}
|
|
|
if (!e?.year || !e?.month || !e?.day) {
|
|
|
- errors[i] = 'Please select visit dates';
|
|
|
+ errors[id] = 'Please select visit dates';
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
@@ -199,7 +215,7 @@ const AddNewTripScreen = ({ route }: { route: any }) => {
|
|
|
const eM = moment(`${e.year}-${e.month}-${e.day}`, 'YYYY-M-D');
|
|
|
|
|
|
if (sM.isAfter(eM)) {
|
|
|
- errors[i] = 'Start date cannot be after end date';
|
|
|
+ errors[id] = 'Start date cannot be after end date';
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
@@ -225,69 +241,65 @@ const AddNewTripScreen = ({ route }: { route: any }) => {
|
|
|
|
|
|
const openRangeCalendarForRegion = (index: number) => {
|
|
|
if (!regions) return;
|
|
|
-
|
|
|
- const prevEnd = regions[index - 1]?.visitEndDate;
|
|
|
- const nextStart = regions[index + 1]?.visitStartDate;
|
|
|
-
|
|
|
- const min = isFullDate(prevEnd) ? dateValueToISO(prevEnd) : undefined;
|
|
|
- const max = isFullDate(nextStart) ? dateValueToISO(nextStart) : undefined;
|
|
|
-
|
|
|
- setCalendarMinDate(min as never);
|
|
|
- setCalendarMaxDate(max as never);
|
|
|
setCalendarVisibleForIndex(index);
|
|
|
};
|
|
|
|
|
|
const closeRangeCalendar = (startDate?: string | null, endDate?: string | null) => {
|
|
|
- const idx = calendarVisibleForIndex;
|
|
|
+ const clickedInstanceIndex = calendarVisibleForIndex;
|
|
|
setCalendarVisibleForIndex(null);
|
|
|
- setCalendarMinDate(undefined);
|
|
|
- setCalendarMaxDate(undefined);
|
|
|
-
|
|
|
- if (idx === null || idx === undefined) return;
|
|
|
|
|
|
- if (!startDate) return;
|
|
|
+ if (clickedInstanceIndex === null || clickedInstanceIndex === undefined || !startDate) return;
|
|
|
|
|
|
const startVal = parseISOToDateValue(startDate);
|
|
|
const endVal = parseISOToDateValue(endDate ?? startDate);
|
|
|
|
|
|
if (!startVal || !endVal) return;
|
|
|
|
|
|
- setRegions((prev) => {
|
|
|
- if (!prev) return prev;
|
|
|
- const updated = [...prev];
|
|
|
+ const openedInstanceId = regions?.[clickedInstanceIndex]?._instanceId;
|
|
|
+ if (!openedInstanceId) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- updated[idx] = {
|
|
|
- ...updated[idx],
|
|
|
- visitStartDate: startVal,
|
|
|
- visitEndDate: endVal
|
|
|
- };
|
|
|
+ const updatedBeforeSort = (regions ?? []).map((r) =>
|
|
|
+ r._instanceId === openedInstanceId
|
|
|
+ ? { ...r, visitStartDate: startVal, visitEndDate: endVal }
|
|
|
+ : r
|
|
|
+ );
|
|
|
|
|
|
- const next = updated[idx + 1];
|
|
|
- if (next && (!next.visitStartDate?.year || !next.visitEndDate?.year)) {
|
|
|
- updated[idx + 1] = {
|
|
|
+ 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 prevR = updated[idx - 1];
|
|
|
- if (prevR && (!prevR.visitStartDate?.year || !prevR.visitEndDate?.year)) {
|
|
|
- updated[idx - 1] = {
|
|
|
- ...prevR,
|
|
|
+ const prev = sorted[newIndex - 1];
|
|
|
+ if (prev && !isFullDate(prev.visitStartDate)) {
|
|
|
+ sorted[newIndex - 1] = {
|
|
|
+ ...prev,
|
|
|
visitStartDate: startVal,
|
|
|
visitEndDate: startVal
|
|
|
};
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- validateRegionsDates(updated);
|
|
|
+ setRegions(sorted);
|
|
|
|
|
|
- return updated;
|
|
|
- });
|
|
|
+ validateRegionsDates(sorted);
|
|
|
|
|
|
setRegionErrors((prev) => {
|
|
|
const clone = { ...prev };
|
|
|
- delete clone[idx];
|
|
|
+ delete clone[clickedInstanceIndex];
|
|
|
return clone;
|
|
|
});
|
|
|
};
|
|
|
@@ -339,6 +351,7 @@ const AddNewTripScreen = ({ route }: { route: any }) => {
|
|
|
const handleSaveNewTrip = () => {
|
|
|
if (regions) {
|
|
|
if (!validateRegionsDates(regions)) {
|
|
|
+ scrollToError(regionErrors);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
@@ -391,6 +404,7 @@ const AddNewTripScreen = ({ route }: { route: any }) => {
|
|
|
const handleUpdateTrip = () => {
|
|
|
if (regions) {
|
|
|
if (!validateRegionsDates(regions)) {
|
|
|
+ scrollToError(regionErrors);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
@@ -445,6 +459,7 @@ const AddNewTripScreen = ({ route }: { route: any }) => {
|
|
|
<PageWrapper style={{ flex: 1 }}>
|
|
|
<Header label={editTripId ? 'Edit Trip' : 'Add New Trip'} />
|
|
|
<ScrollView
|
|
|
+ ref={scrollRef}
|
|
|
contentContainerStyle={{ flexGrow: 1, gap: 16 }}
|
|
|
showsVerticalScrollIndicator={false}
|
|
|
>
|
|
|
@@ -460,19 +475,6 @@ const AddNewTripScreen = ({ route }: { route: any }) => {
|
|
|
|
|
|
<View style={{ marginBottom: 8 }}>
|
|
|
<Text style={styles.regionsLabel}>Regions</Text>
|
|
|
- <TouchableOpacity
|
|
|
- style={styles.addRegionBtn}
|
|
|
- onPress={() =>
|
|
|
- navigation.navigate(
|
|
|
- ...([
|
|
|
- NAVIGATION_PAGES.ADD_REGIONS_NEW,
|
|
|
- { regionsParams: regions, editId: editTripId }
|
|
|
- ] as never)
|
|
|
- )
|
|
|
- }
|
|
|
- >
|
|
|
- <Text style={styles.addRegionBtntext}>Add visit</Text>
|
|
|
- </TouchableOpacity>
|
|
|
{regions && regions.length ? (
|
|
|
<View style={styles.regionsContainer}>
|
|
|
{regions.map((region, index) => {
|
|
|
@@ -484,7 +486,7 @@ const AddNewTripScreen = ({ route }: { route: any }) => {
|
|
|
dateValueToISO(region.visitStartDate) === dateValueToISO(region.visitEndDate)
|
|
|
? startLabel
|
|
|
: region.visitStartDate?.year && region.visitEndDate?.year
|
|
|
- ? `${startLabel} – ${endLabel}`
|
|
|
+ ? `${startLabel} - ${endLabel}`
|
|
|
: 'Select visit dates';
|
|
|
|
|
|
return (
|
|
|
@@ -498,23 +500,12 @@ const AddNewTripScreen = ({ route }: { route: any }) => {
|
|
|
datesLabel={datesLabel}
|
|
|
onMoveUp={() => moveRegionUp(index)}
|
|
|
onMoveDown={() => moveRegionDown(index)}
|
|
|
- errorMessage={regionErrors[index]}
|
|
|
+ errorMessage={regionErrors[region?._instanceId ?? index]}
|
|
|
+ startLabel={startLabel}
|
|
|
+ endLabel={endLabel}
|
|
|
/>
|
|
|
);
|
|
|
})}
|
|
|
- <TouchableOpacity
|
|
|
- style={[styles.addRegionBtn, { marginTop: 0 }]}
|
|
|
- onPress={() =>
|
|
|
- navigation.navigate(
|
|
|
- ...([
|
|
|
- NAVIGATION_PAGES.ADD_REGIONS_NEW,
|
|
|
- { regionsParams: regions, editId: editTripId }
|
|
|
- ] as never)
|
|
|
- )
|
|
|
- }
|
|
|
- >
|
|
|
- <Text style={styles.addRegionBtntext}>Add visit</Text>
|
|
|
- </TouchableOpacity>
|
|
|
</View>
|
|
|
) : (
|
|
|
<Text style={styles.noRegiosText}>No regions at the moment</Text>
|
|
|
@@ -522,60 +513,73 @@ const AddNewTripScreen = ({ route }: { route: any }) => {
|
|
|
</View>
|
|
|
</ScrollView>
|
|
|
|
|
|
- <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>
|
|
|
+ <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 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 }
|
|
|
+ disabled && { backgroundColor: Colors.LIGHT_GRAY, borderColor: Colors.LIGHT_GRAY },
|
|
|
+ { paddingVertical: 12 }
|
|
|
]}
|
|
|
- onPress={handleUpdateTrip}
|
|
|
- disabled={disabled || isLoading === 'update'}
|
|
|
+ onPress={handleSaveNewTrip}
|
|
|
+ disabled={disabled || isLoading === 'save'}
|
|
|
>
|
|
|
- {isLoading === 'update' ? (
|
|
|
+ {isLoading === 'save' ? (
|
|
|
<ActivityIndicator size={18} color={Colors.WHITE} />
|
|
|
) : (
|
|
|
- <Text style={[styles.tabText, styles.addNewTabText]}>Save Trip</Text>
|
|
|
+ <Text style={[styles.tabText, styles.addNewTabText]}>Save New 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]}>Add New Trip</Text>
|
|
|
- )}
|
|
|
- </TouchableOpacity>
|
|
|
- )}
|
|
|
+ )}
|
|
|
+ </View>
|
|
|
</View>
|
|
|
|
|
|
<RangeCalendar
|
|
|
isModalVisible={calendarVisibleForIndex !== null}
|
|
|
- minDate={calendarMinDate}
|
|
|
- maxDate={calendarMaxDate}
|
|
|
allowRangeSelection={true}
|
|
|
closeModal={(startDate?: string | null, endDate?: string | null) =>
|
|
|
closeRangeCalendar(startDate, endDate)
|