|
|
@@ -1,88 +1,342 @@
|
|
|
-import React, { useState, useEffect } from 'react';
|
|
|
-import { View, StyleSheet, Platform, Text } from 'react-native';
|
|
|
+import React, { useState, useEffect, useRef } from 'react';
|
|
|
+import {
|
|
|
+ View,
|
|
|
+ StyleSheet,
|
|
|
+ Platform,
|
|
|
+ Text,
|
|
|
+ TouchableOpacity,
|
|
|
+ Animated,
|
|
|
+ LayoutChangeEvent
|
|
|
+} from 'react-native';
|
|
|
import { Picker as WheelPicker } from 'react-native-wheel-pick';
|
|
|
+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 { Modal } from '../Modal';
|
|
|
import { Button } from '../Button';
|
|
|
import { ButtonVariants } from 'src/types/components';
|
|
|
import { Colors } from 'src/theme';
|
|
|
+import Navigation from '../Calendars/RangeCalendar/Navigation';
|
|
|
+
|
|
|
+export type CalendarMode = 'exact' | 'approx';
|
|
|
|
|
|
interface AddVisitModalProps {
|
|
|
isVisible: boolean;
|
|
|
onClose: () => void;
|
|
|
regionName: string;
|
|
|
- onSave: (year: number | null) => void;
|
|
|
+ onSave: (
|
|
|
+ startDate?: string | null,
|
|
|
+ endDate?: string | null,
|
|
|
+ approxYear?: number | null
|
|
|
+ ) => void;
|
|
|
}
|
|
|
|
|
|
-const CURRENT_YEAR = new Date().getFullYear();
|
|
|
+export const isVisitInFuture = (
|
|
|
+ startDate?: string | null,
|
|
|
+ endDate?: string | null,
|
|
|
+ approxYear?: number | null
|
|
|
+): boolean => {
|
|
|
+ const currentYear = dayjs().year();
|
|
|
+ const todayISO = dayjs().format('YYYY-MM-DD');
|
|
|
+
|
|
|
+ if (approxYear !== undefined && approxYear !== null) {
|
|
|
+ return approxYear > currentYear;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (startDate) {
|
|
|
+ if (!startDate.includes('-') && !isNaN(Number(startDate))) {
|
|
|
+ return Number(startDate) > currentYear;
|
|
|
+ }
|
|
|
+ return startDate > todayISO;
|
|
|
+ }
|
|
|
+
|
|
|
+ return false;
|
|
|
+};
|
|
|
+
|
|
|
+const CURRENT_YEAR = dayjs().year();
|
|
|
const YEARS_DATA = [
|
|
|
'No year',
|
|
|
...Array.from({ length: 100 }, (_, i) => String(CURRENT_YEAR - 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'
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
export const AddVisitModal: React.FC<AddVisitModalProps> = ({
|
|
|
isVisible,
|
|
|
onClose,
|
|
|
regionName,
|
|
|
onSave
|
|
|
}) => {
|
|
|
+ const [activeTab, setActiveTab] = useState<CalendarMode>('approx');
|
|
|
const [selectedYear, setSelectedYear] = useState<number | null>(CURRENT_YEAR);
|
|
|
|
|
|
+ const [selectedStartDate, setSelectedStartDate] = useState<string | null>(null);
|
|
|
+ const [selectedEndDate, setSelectedEndDate] = useState<string | null>(null);
|
|
|
+ const [startDate, setStartDate] = useState<DateType>(undefined);
|
|
|
+ const [endDate, setEndDate] = useState<DateType>(undefined);
|
|
|
+
|
|
|
useEffect(() => {
|
|
|
if (isVisible) {
|
|
|
+ setActiveTab('approx');
|
|
|
setSelectedYear(CURRENT_YEAR);
|
|
|
+ setSelectedStartDate(null);
|
|
|
+ setSelectedEndDate(null);
|
|
|
+ setStartDate(undefined);
|
|
|
+ setEndDate(undefined);
|
|
|
}
|
|
|
}, [isVisible]);
|
|
|
|
|
|
+ const handleDateChange = (params: any) => {
|
|
|
+ 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);
|
|
|
+ };
|
|
|
+
|
|
|
const handleConfirm = () => {
|
|
|
- onSave(selectedYear);
|
|
|
+ if (activeTab === 'approx') {
|
|
|
+ onSave(null, null, selectedYear);
|
|
|
+ } else {
|
|
|
+ onSave(selectedStartDate, selectedEndDate, null);
|
|
|
+ }
|
|
|
onClose();
|
|
|
};
|
|
|
|
|
|
+ const isDoneEnabled = activeTab === 'approx' ? true : !!selectedStartDate;
|
|
|
+
|
|
|
return (
|
|
|
<Modal
|
|
|
visible={isVisible}
|
|
|
onRequestClose={onClose}
|
|
|
- headerTitle="Select year"
|
|
|
+ headerTitle="Select Dates"
|
|
|
visibleInPercent={'auto'}
|
|
|
>
|
|
|
- <View style={[styles.content, Platform.OS === 'android' ? { maxHeight: 400, paddingTop: 0 } : {}]}>
|
|
|
- <Text style={[styles.regionText, Platform.OS === 'android' ? { marginBottom: 16 } : {}]}>{regionName}</Text>
|
|
|
- <WheelPicker
|
|
|
- style={[styles.wheel, Platform.OS === 'android' ? { height: '75%' } : { height: 280 }]}
|
|
|
- pickerData={YEARS_DATA}
|
|
|
- selectedValue={selectedYear === null ? 'No year' : String(selectedYear)}
|
|
|
- onValueChange={(value: string) => {
|
|
|
- if (value === 'No year') {
|
|
|
- setSelectedYear(null);
|
|
|
- } else {
|
|
|
- setSelectedYear(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: 280,
|
|
|
- }}
|
|
|
- selectedItemTextStyle={{
|
|
|
- fontSize: 30,
|
|
|
- fontFamily: 'montserrat-700',
|
|
|
- color: Colors.DARK_BLUE
|
|
|
- }}
|
|
|
- />
|
|
|
+ <View style={styles.content}>
|
|
|
+ {!!regionName && <Text style={styles.regionText}>{regionName}</Text>}
|
|
|
+ <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'}
|
|
|
+ >
|
|
|
+ <DateTimePicker
|
|
|
+ mode="range"
|
|
|
+ startDate={startDate}
|
|
|
+ endDate={endDate}
|
|
|
+ onChange={handleDateChange}
|
|
|
+ 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 },
|
|
|
+ Platform.OS === 'android' ? { justifyContent: 'center' } : {}
|
|
|
+ ]}
|
|
|
+ pointerEvents={activeTab === 'approx' ? 'auto' : 'none'}
|
|
|
+ >
|
|
|
+ <WheelPicker
|
|
|
+ style={[styles.wheel, Platform.OS === 'android' ? { height: '75%' } : {}]}
|
|
|
+ pickerData={YEARS_DATA}
|
|
|
+ selectedValue={selectedYear === null ? 'No year' : String(selectedYear)}
|
|
|
+ onValueChange={(value: string) => {
|
|
|
+ if (value === 'No year') {
|
|
|
+ setSelectedYear(null);
|
|
|
+ } else {
|
|
|
+ setSelectedYear(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: FIXED_CONTENT_HEIGHT
|
|
|
+ }}
|
|
|
+ selectedItemTextStyle={{
|
|
|
+ fontSize: 30,
|
|
|
+ fontFamily: 'montserrat-700',
|
|
|
+ color: Colors.DARK_BLUE
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ </View>
|
|
|
+ </View>
|
|
|
</View>
|
|
|
<View style={styles.modalFooter}>
|
|
|
<Button
|
|
|
children="Add visit"
|
|
|
onPress={handleConfirm}
|
|
|
- variant={ButtonVariants.FILL}
|
|
|
+ disabled={!isDoneEnabled}
|
|
|
+ variant={!isDoneEnabled ? ButtonVariants.OPACITY : ButtonVariants.FILL}
|
|
|
containerStyles={{ borderWidth: 0 }}
|
|
|
/>
|
|
|
</View>
|
|
|
@@ -93,25 +347,24 @@ export const AddVisitModal: React.FC<AddVisitModalProps> = ({
|
|
|
const styles = StyleSheet.create({
|
|
|
content: {
|
|
|
backgroundColor: Colors.WHITE,
|
|
|
- justifyContent: 'center',
|
|
|
- alignItems: 'center',
|
|
|
paddingTop: 16,
|
|
|
- paddingBottom: 8,
|
|
|
+ paddingBottom: 8
|
|
|
},
|
|
|
regionText: {
|
|
|
fontSize: 14,
|
|
|
fontFamily: 'montserrat-600',
|
|
|
color: Colors.DARK_BLUE,
|
|
|
textAlign: 'center',
|
|
|
- paddingHorizontal: 24
|
|
|
+ paddingHorizontal: 24,
|
|
|
+ marginBottom: 12
|
|
|
},
|
|
|
wheel: {
|
|
|
width: '100%',
|
|
|
- backgroundColor: 'transparent',
|
|
|
+ backgroundColor: 'transparent'
|
|
|
},
|
|
|
modalFooter: {
|
|
|
justifyContent: 'flex-end',
|
|
|
width: '100%',
|
|
|
- marginBottom: 24,
|
|
|
- },
|
|
|
+ marginBottom: 24
|
|
|
+ }
|
|
|
});
|