import { View, Text, ScrollView, Pressable, StyleSheet, Image, Dimensions, ActivityIndicator, Platform, Linking, Alert } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { router, useLocalSearchParams } from 'expo-router'; import NormalButton from '../global/normal_button'; import { CheckMarkLogoSvg, DirectionLogoSvg, PreviousPageBlackSvg } from '../global/SVG'; import { ChargingStationTabView } from '../global/chargingStationTabView'; import ChooseCarForChargingRow from '../global/chooseCarForChargingRow'; import { useEffect, useState } from 'react'; import DropdownSelect from '../global/dropdown_select'; import { chargeStationService } from '../../service/chargeStationService'; import { authenticationService } from '../../service/authService'; import * as Location from 'expo-location'; import { calculateDistance } from '../global/distanceCalculator'; import Modal from 'react-native-modal'; import useUserInfoStore from '../../providers/userinfo_store'; interface AccordionItemProps { title: string; children: React.ReactNode; isOpen: boolean; onToggle: () => void; isSelected: boolean; } const AccordionItem: React.FC = ({ title, children, isOpen, onToggle, isSelected }) => ( {title} {isOpen && {children}} ); const MakingBookingPageComponent = () => { const [isModalVisible, setModalVisible] = useState(false); const [routerParams, setRouterParams] = useState(null); const [openDrawer, setOpenDrawer] = useState(1); const [selectedTime, setSelectedTime] = useState(''); const [availableTimeSlots, setAvailableTimeSlots] = useState([]); const [selectedDrawer, setSelectedDrawer] = useState(1); const [isLoading, setIsLoading] = useState(true); const [selectedDate, setSelectedDate] = useState(''); const toggleDrawer = (index: number) => { setOpenDrawer(openDrawer === index ? null : index); setSelectedDrawer(index); }; const [defaultCar, setDefaultCar] = useState(null); const [isDefaultCarLoading, setIsDefaultCarLoading] = useState(true); const [availableDate, setAvailableDate] = useState([]); const [car, setCar] = useState([]); const [selectedCarID, setSelectedCarID] = useState(''); const [selectedChargingGun, setSelectedChargingGun] = useState(''); const [chargingBasedOnWatt, setChargingBasedOnWatt] = useState(true); const [stopChargingUponBatteryFull, setStopChargingUponBatteryFull] = useState(false); const [selectedCar, setSelectedCar] = useState(''); const [selectedDuration, setSelectedDuration] = useState(''); const { width: screenWidth, height: screenHeight } = Dimensions.get('window'); const [price, setPrice] = useState(0); const layoutWidth = screenWidth; const layoutHeight = screenHeight * 0.32; const { userID, setUserID } = useUserInfoStore(); const params = useLocalSearchParams(); const chargeStationID = params.chargeStationID as string; const chargeStationName = params.chargeStationName as string; const chargeStationAddress = params.chargeStationAddress as string; const chargeStationLat = params.chargeStationLat as string; const chargeStationLng = params.chargeStationLng as string; const [selectedWatt, setSelectedWatt] = useState(''); const [availableConnectorDropdownOptions, setAvailableConnectorDropdownOptions] = useState([]); const [carCapacitance, setCarCapacitance] = useState(''); const [currentLocation, setCurrentLocation] = useState(null); const [distance, setDistance] = useState(null); const [upcomingReservations, setUpcomingReservation] = useState([]); const [carLoadingState, setCarLoadingState] = useState(false); const [isDateLoading, setIsDateLoading] = useState(true); useEffect(() => { const getCurrentLocation = async () => { let { status } = await Location.requestForegroundPermissionsAsync(); if (status !== 'granted') { console.error('Permission to access location was denied'); return; } let location = await Location.getLastKnownPositionAsync({}); setCurrentLocation(location); }; getCurrentLocation(); }, []); //getDefaultCar useEffect(() => { const fetchDefaultCar = async () => { setIsDefaultCarLoading(true); try { const response = await chargeStationService.getUserDefaultCars(); if (response && response.data) { setDefaultCar(response.data.id); setSelectedCarID(response.data.id); setCarCapacitance(response.data.car_type.capacitance); setSelectedCar(response.data.id); console.log('*******', response.data.car_type.capacitance); } } catch (error) { console.log('Error fetching default car:', error); } finally { setIsDefaultCarLoading(false); } }; fetchDefaultCar(); }, []); const formatDistance = (distanceInMeters: number): string => { if (distanceInMeters < 1000) { return `${Math.round(distanceInMeters)}米`; } else { const distanceInKm = distanceInMeters / 1000; return `${distanceInKm.toFixed(1)}公里`; } }; const getUpcomingReservations = (reservations: any, daysAhead = 3) => { const today = new Date(); const threeDaysLater = new Date(today); threeDaysLater.setDate(today.getDate() + daysAhead); return reservations .filter((reservation) => { const reservationDate = new Date(reservation.book_time); return reservationDate >= today && reservationDate <= threeDaysLater; }) .sort((a, b) => new Date(a.reservationDate) - new Date(b.reservationDate)); }; //USE BELOW to find upcoming reservations, then I filter availableDate and time based on the upcoming ones so user cannot book the same time twice. const formatReservations = (reservations) => { const formattedReservations = {}; reservations.forEach((reservation) => { const bookTime = new Date(reservation.book_time); const date = formatDate(bookTime); const time = formatTime(bookTime); if (!formattedReservations[date]) { formattedReservations[date] = []; } formattedReservations[date].push(time); }); return Object.entries(formattedReservations).map(([date, times]) => ({ date, times })); }; const formatDate = (date) => { const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${month}/${day}`; }; const formatTime = (date) => { return date.toTimeString().slice(0, 5); }; useEffect(() => { const fetchReservationHistories = async () => { try { const response = await chargeStationService.fetchReservationHistories(); if (response) { // console.log('response', response); // console.log('Reservation histories:', response); const upcomingReservations = getUpcomingReservations(response); // console.log('upcomingReservations', upcomingReservations); const formattedReservations = formatReservations(upcomingReservations); // console.log('formattedReservations', formattedReservations); setUpcomingReservation(formattedReservations); } 2; } catch (error) { console.log(error); } }; fetchReservationHistories(); }, []); //USE ABOVE to find upcoming reservations, then I filter availableDate and time based on the upcoming ones so user cannot book the same time twice. useEffect(() => { const getDistance = async () => { if (currentLocation) { try { const distance = await calculateDistance( Number(params.chargeStationLat), Number(params.chargeStationLng), currentLocation ); setDistance(formatDistance(distance)); } catch (error) { console.error('Error calculating distance:', error); } } }; getDistance(); }, [params.chargeStationLat, params.chargeStationLng, currentLocation]); useEffect(() => { const fetchPrice = async () => { try { const price = await chargeStationService.fetchChargeStationPrice(chargeStationID); setPrice(price); } catch (error) { console.error('Error fetching price:', error); } }; fetchPrice(); }, []); function appendImageUrlToCarResult(carData, updatedVehicles) { return carData.map((car) => { const matchingVehicle = updatedVehicles.find((vehicle) => vehicle.car_type.name === car.car_name); if (matchingVehicle) { return { ...car, type_image_url: matchingVehicle.car_type.type_image_url }; } return car; }); } // useEffect(() => { // setCarLoadingState(true); // const fetchUserInfoAndCarData = async () => { // try { // const fetchedUserInfo = await authenticationService.getUserInfo(); // const userData = fetchedUserInfo?.data; // // console.log('userData', userData); // if (userData) { // setUserID(userData.id); // const carData = userData.cars.map((car: any) => ({ // car_name: car.name, // car_capacitance: car.capacitance, // car_id: car.id, // isDefault: car.id === userData.defaultCar?.id // })); // // console.log('carDarta', carData); // setCar(carData); // const carResult = await chargeStationService.getUserCars(); // let updatedVehicles = [...carResult.data]; // // console.log('updatedVehiaacles', updatedVehicles); // const updatedCarResult = appendImageUrlToCarResult(carData, updatedVehicles); // setCar(updatedCarResult); // let updatedCarResultWithProcessedUrl = [...updatedCarResult]; // for (let i = 0; i < updatedCarResultWithProcessedUrl.length; i++) { // const car = updatedCarResultWithProcessedUrl[i]; // const processedUrl = await chargeStationService.getProcessedImageUrl(car.type_image_url); // updatedCarResultWithProcessedUrl[i] = { // ...car, // processedImageUrl: processedUrl // }; // } // // console.log(updatedCarResultWithProcessedUrl); // setCar(updatedCarResultWithProcessedUrl); // } // } catch (error) { // console.error('Error fetching user info:', error); // } finally { // setCarLoadingState(false); // } // }; // fetchUserInfoAndCarData(); // }, []); useEffect(() => { const fetchingAvailableTimeSlots = async () => { setIsLoading(true); try { const fetchedTimeSlots = await chargeStationService.fetchAvailableTimeSlots( chargeStationID, selectedDate ); const now = new Date(); const today = `${String(now.getMonth() + 1).padStart(2, '0')}/${String(now.getDate()).padStart( 2, '0' )}`; let filteredTimeSlots = fetchedTimeSlots; //filter out today's time slots that have already passed if (selectedDate === today) { const currentHours = now.getHours(); const currentMinutes = now.getMinutes(); filteredTimeSlots = fetchedTimeSlots.filter((time) => { const [hours, minutes] = time.split(':').map(Number); if (hours > currentHours) return true; if (hours === currentHours && minutes > currentMinutes) return true; return false; }); } //filter out time slots that are already fully booked const reservedSlotsForDate = upcomingReservations.find((res) => res.date === selectedDate); if (reservedSlotsForDate) { filteredTimeSlots = filteredTimeSlots.filter((time) => !reservedSlotsForDate.times.includes(time)); } setAvailableTimeSlots(filteredTimeSlots); } catch (error) { console.error('Error fetching time slots:', error); } finally { setIsLoading(false); } }; if (selectedDate) { fetchingAvailableTimeSlots(); } }, [selectedDate]); useEffect(() => { const fetchConnectorOptions = async () => { try { const fetchedData = await chargeStationService.fetchSpecificChargeStation(chargeStationID); console.log('fetchedDate', fetchedData); const dateObject = fetchedData.find((item) => item.date === selectedDate); console.log('dateObject', dateObject); if (!dateObject) { setAvailableConnectorDropdownOptions([]); return; } const rangeObject = dateObject.range.find((range) => range.start === selectedTime); console.log('rangeObject', rangeObject); if (!rangeObject) { setAvailableConnectorDropdownOptions([]); return; } const connectorIDs = rangeObject.connectors .filter((connector) => connector.Status === '待机') .map((connector) => connector.ConnectorID); console.log('connectorIDs', connectorIDs); setAvailableConnectorDropdownOptions(connectorIDs); } catch (error) { console.error('Error fetching charge station data:', error); setAvailableConnectorDropdownOptions([]); } }; fetchConnectorOptions(); }, [chargeStationID, selectedDate, selectedTime]); // old // const formattedConnectorDropdownOptions = availableConnectorDropdownOptions.map((id, index) => ({ // label: (index + 1).toString(), // value: id // })); const connectorIDToLabelMap = { '101708240502475001': '1', '101708240502476001': '2', '101708240502477001': '3', '101708240502478001': '4', '101708240502474001': '5', '101708240502474002': '6' }; console.log('availableConnectorDropdownOptions', availableConnectorDropdownOptions); const formattedConnectorDropdownOptions = availableConnectorDropdownOptions .map((id) => ({ label: connectorIDToLabelMap[id] || '', value: id })) .filter((option) => option.label !== '') .sort((a, b) => parseInt(a.label) - parseInt(b.label)); console.log('formattedConnectorDropdownOptions', formattedConnectorDropdownOptions); useEffect(() => { const fetchingAvailableDates = async () => { setIsDateLoading(true); try { const fetchedDates = await chargeStationService.fetchAvailableDates(chargeStationID); console.log('fetchedDates', fetchedDates); setAvailableDate(fetchedDates); console.log(fetchedDates.slice(0, 3)); } catch (error) { console.error('Error fetching available dates:', error); } finally { setIsDateLoading(false); } }; fetchingAvailableDates(); }, [chargeStationID]); const handleNavigationPress = () => { const latitude = chargeStationLat; const longitude = chargeStationLng; // console.log('latitude', latitude); // console.log('longitude', longitude); const label = encodeURIComponent(chargeStationName); // Google Maps App URL const googleMapsUrl = `https://www.google.com/maps/search/?api=1&query=${latitude},${longitude}`; // Fallback URL for web browser const webUrl = `https://www.google.com/maps/dir/?api=1&destination=${latitude},${longitude}`; Linking.canOpenURL(googleMapsUrl) .then((supported) => { if (supported) { Linking.openURL(googleMapsUrl); } else { Linking.openURL(webUrl).catch((err) => { console.error('An error occurred', err); Alert.alert( 'Error', "Unable to open Google Maps. Please make sure it's installed on your device.", [{ text: 'OK' }], { cancelable: false } ); }); } }) .catch((err) => console.error('An error occurred', err)); }; const handleConfirmation = (value: any) => { setModalVisible(true); const selectedOption = formattedConnectorDropdownOptions.find((option) => option.value === value); const label = selectedOption ? selectedOption.label : ''; setRouterParams({ pathname: '/bookingConfirmationPage', params: { chargeStationName, chargeStationAddress, chargeStationID, connectorID: value, connectorLabel: label, userID, carCapacitance: carCapacitance, carID: selectedCarID, carName: selectedCar, date: selectedDate, bookTime: selectedTime, chargingMethod: stopChargingUponBatteryFull ? 'stopChargingUponBatteryFull' : 'chargingBasedOnWatt', chargingWatt: selectedWatt || '', price: price } }); }; const handleModalConfirm = () => { // Alert.alert('提示', '此功能正在準備中,敬請期待', [{ text: '確定', onPress: () => router.push('/mainPage') }], { // cancelable: false // }); setModalVisible(false); if (routerParams) { router.push(routerParams); } }; return ( { if (router.canGoBack()) { router.back(); } else { router.replace('./'); } }} > {chargeStationName} {chargeStationAddress} 路線 } // onPress={() => console.log('路線')} onPress={handleNavigationPress} extendedStyle={{ backgroundColor: '#E3F2F8', borderRadius: 61, paddingHorizontal: 20, paddingVertical: 8 }} /> Walk-In {/* {distance} */} {/* {selectedCar !== '' ? ( <> { setSelectedCar(''); setSelectedWatt(''); setOpenDrawer(0); setSelectedDrawer(0); setSelectedDuration(''); setChargingBasedOnWatt(false); setStopChargingUponBatteryFull(false); setSelectedDate(''); setSelectedTime(''); }} > 選擇充電車輛 {selectedCar} ) : ( toggleDrawer(0)} isSelected={selectedDrawer === 0} > {carLoadingState ? ( ) : ( {car .sort((a, b) => (b.isDefault ? 1 : 0) - (a.isDefault ? 1 : 0)) .map((car, index) => ( { setSelectedCar(car.car_name); setSelectedCarID(car.car_id); setCarCapacitance(car.car_capacitance); setSelectedDrawer(1); setOpenDrawer(1); }} image={car.processedImageUrl} key={`${car.car_name}+${index}`} VehicleName={car.car_name} isDefault={car.isDefault} /> ))} )} )} */} {stopChargingUponBatteryFull === true || selectedWatt !== '' ? ( { setSelectedDuration(''); setChargingBasedOnWatt(false); setStopChargingUponBatteryFull(false); setSelectedTime(''); setSelectedDate(''); setSelectedWatt(''); setOpenDrawer(1); setSelectedDrawer(1); }} > 選擇充電方案 {selectedWatt !== '' ? `按每道電 - ${selectedWatt.split('~')[0]}` : '充滿停機'} ) : ( {}} isSelected={selectedDrawer === 1} > { setChargingBasedOnWatt(!chargingBasedOnWatt); setStopChargingUponBatteryFull(false); }} > 按每度電 {/* { setStopChargingUponBatteryFull(!stopChargingUponBatteryFull); setChargingBasedOnWatt(false); setSelectedDrawer(2); setOpenDrawer(2); }} className={`border rounded-lg border-[#34667c] w-[47%] items-center bg-white ${ stopChargingUponBatteryFull ? ' bg-[#34667c]' : '' }`} > 充滿停機 */} {chargingBasedOnWatt === true && ( {['20 kWh~25mins', '25 kWh~30mins', '30 kWh~40mins', '40 kWh~45mins'].map( (watt) => ( { setSelectedWatt(watt); setOpenDrawer(2); setSelectedDrawer(2); }} > {watt.split('~')[0]} {watt.split('~')[1]} ) )} )} )} {selectedTime !== '' ? ( { setOpenDrawer(2); setSelectedDrawer(2); setSelectedDate(''); setSelectedTime(''); }} > 選擇日期 {selectedDate} - {selectedTime} ) : ( { if (stopChargingUponBatteryFull !== false || selectedDuration !== '') { toggleDrawer(2); } }} isSelected={selectedDrawer === 2} > {isDateLoading ? ( ) : ( {availableDate.slice(0, 3).map((date) => ( { setSelectedDate(date); }} > {date} ))} )} {selectedDate !== '' && ( <> 選擇時間 {isLoading ? ( ) : ( {availableTimeSlots.map((time) => ( { setSelectedTime(time); setOpenDrawer(3); setSelectedDrawer(3); }} > {time} ))} )} )} )} { if (selectedTime) { // toggleDrawer(3); } }} isSelected={selectedDrawer === 3} > { setSelectedChargingGun(value); handleConfirmation(value); }} extendedStyle={{ borderColor: '#34667c', marginTop: 4, padding: 12 }} /> setModalVisible(false)} backdropOpacity={0.5} animationIn="fadeIn" animationOut="fadeOut" > 若客戶逾時超過15分鐘,系統將視作自動放棄預約,客戶需要重新預約一次。 本公司有權保留全數費用,恕不退還。按下確認代表您已閱讀並同意上述條款。 我確認} onPress={handleModalConfirm} extendedStyle={styles.confirmButton} /> {/* {routerParams ? ( 新增 } onPress={() => handleConfirmation(selectedChargingGun)} /> ) : ( '' )} */} 充電站資訊 ); }; export default MakingBookingPageComponent; const styles = StyleSheet.create({ grayColor: { color: '#888888' }, topLeftTriangle: { width: 0, height: 0, borderLeftWidth: 50, borderBottomWidth: 50, borderLeftColor: '#02677D', borderBottomColor: 'transparent', position: 'absolute', top: 0, left: 0 }, modalContent: { backgroundColor: 'white', padding: 22, justifyContent: 'center', alignItems: 'center', borderRadius: 4, borderColor: 'rgba(0, 0, 0, 0.1)' }, modalText: { fontSize: 18, marginBottom: 12, textAlign: 'center' }, confirmButton: { backgroundColor: '#34667c', paddingHorizontal: 30, paddingVertical: 10, borderRadius: 5 }, text: { fontWeight: 300, color: '#000000' } });