makingBookingPageComponent.tsx 57 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192
  1. import {
  2. View,
  3. Text,
  4. ScrollView,
  5. Pressable,
  6. StyleSheet,
  7. Image,
  8. Dimensions,
  9. ActivityIndicator,
  10. Platform,
  11. Linking,
  12. Alert
  13. } from 'react-native';
  14. import { SafeAreaView } from 'react-native-safe-area-context';
  15. import { router, useLocalSearchParams } from 'expo-router';
  16. import NormalButton from '../global/normal_button';
  17. import { CheckMarkLogoSvg, DirectionLogoSvg, PreviousPageBlackSvg } from '../global/SVG';
  18. import { ChargingStationTabView } from '../global/chargingStationTabView';
  19. import { useEffect, useState } from 'react';
  20. import DropdownSelect from '../global/dropdown_select';
  21. import { chargeStationService } from '../../service/chargeStationService';
  22. import * as Location from 'expo-location';
  23. import { calculateDistance } from '../global/distanceCalculator';
  24. import Modal from 'react-native-modal';
  25. import useUserInfoStore from '../../providers/userinfo_store';
  26. interface AccordionItemProps {
  27. title: string;
  28. children: React.ReactNode;
  29. isOpen: boolean;
  30. onToggle: () => void;
  31. isSelected: boolean;
  32. }
  33. const AccordionItem: React.FC<AccordionItemProps> = ({ title, children, isOpen, onToggle, isSelected }) => (
  34. <View className={`${isSelected ? 'bg-[#e7f5f8]' : 'bg-white'}`}>
  35. <View className="mx-[5%]">
  36. <Pressable onPress={onToggle}>
  37. <Text className={` pt-2 text-lg ${isSelected ? 'text-[#222222]' : 'text-[#888888] '}}`}>{title}</Text>
  38. </Pressable>
  39. {isOpen && <View>{children}</View>}
  40. </View>
  41. </View>
  42. );
  43. const MakingBookingPageComponent = () => {
  44. const [isModalVisible, setModalVisible] = useState(false);
  45. const [routerParams, setRouterParams] = useState(null);
  46. const [openDrawer, setOpenDrawer] = useState<number | null>(1);
  47. const [selectedTime, setSelectedTime] = useState<string>('');
  48. const [availableTimeSlots, setAvailableTimeSlots] = useState<string[]>([]);
  49. const [selectedDrawer, setSelectedDrawer] = useState<number>(1);
  50. const [isLoading, setIsLoading] = useState(true);
  51. const [selectedDate, setSelectedDate] = useState<string>('');
  52. const toggleDrawer = (index: number) => {
  53. setOpenDrawer(openDrawer === index ? null : index);
  54. setSelectedDrawer(index);
  55. };
  56. const [defaultCar, setDefaultCar] = useState(null);
  57. const [isDefaultCarLoading, setIsDefaultCarLoading] = useState(true);
  58. const [availableDate, setAvailableDate] = useState<string[]>([]);
  59. const [car, setCar] = useState([]);
  60. const [selectedCarID, setSelectedCarID] = useState('');
  61. const [selectedChargingGun, setSelectedChargingGun] = useState('');
  62. const [chargingBasedOnWatt, setChargingBasedOnWatt] = useState(true);
  63. const [stopChargingUponBatteryFull, setStopChargingUponBatteryFull] = useState(false);
  64. const [selectedCar, setSelectedCar] = useState('');
  65. const [selectedDuration, setSelectedDuration] = useState('');
  66. const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
  67. const [price, setPrice] = useState(0);
  68. const layoutWidth = screenWidth;
  69. const layoutHeight = screenHeight * 0.32;
  70. const { userID, setUserID } = useUserInfoStore();
  71. const [formattedDates, setFormattedDates] = useState([]);
  72. const params = useLocalSearchParams();
  73. const chargeStationID = params.chargeStationID as string;
  74. const chargeStationName = params.chargeStationName as string;
  75. const chargeStationAddress = params.chargeStationAddress as string;
  76. const chargeStationLat = params.chargeStationLat as string;
  77. const chargeStationLng = params.chargeStationLng as string;
  78. const [selectedWatt, setSelectedWatt] = useState('');
  79. const [availableConnectorDropdownOptions, setAvailableConnectorDropdownOptions] = useState([]);
  80. const [carCapacitance, setCarCapacitance] = useState('');
  81. const [currentLocation, setCurrentLocation] = useState<Location.LocationObject | null>(null);
  82. const [distance, setDistance] = useState<string | null>(null);
  83. const [upcomingReservations, setUpcomingReservation] = useState([]);
  84. const [carLoadingState, setCarLoadingState] = useState(false);
  85. const [isDateLoading, setIsDateLoading] = useState(false);
  86. const [dataResponse, setDataResponse] = useState([]);
  87. //check for unpaid penalties
  88. useEffect(() => {
  89. const checkUnpaidPenalties = async () => {
  90. try {
  91. const reservationHistories = await chargeStationService.fetchReservationHistories();
  92. const unpaidPenalties = reservationHistories.filter(
  93. (reservation) => reservation.penalty_fee > 0 && reservation.penalty_paid_status === false
  94. );
  95. if (unpaidPenalties.length > 0) {
  96. const mostRecentUnpaidReservation = unpaidPenalties.reduce((mostRecent, current) => {
  97. return new Date(mostRecent.created_at) > new Date(current.created_at) ? mostRecent : current;
  98. }, unpaidPenalties[0]);
  99. Alert.alert(
  100. '未付罰款',
  101. '您有未支付的罰款。請先支付罰款後再開始充電。',
  102. [
  103. {
  104. text: '查看詳情',
  105. onPress: () => {
  106. // Navigate to a page showing penalty details
  107. router.push({
  108. pathname: '(auth)/(tabs)/(home)/penaltyPaymentPage',
  109. params: {
  110. book_time: mostRecentUnpaidReservation.book_time,
  111. end_time: mostRecentUnpaidReservation.end_time,
  112. actual_end_time: mostRecentUnpaidReservation.actual_end_time,
  113. penalty_fee: mostRecentUnpaidReservation.penalty_fee,
  114. format_order_id: mostRecentUnpaidReservation.format_order_id,
  115. id: mostRecentUnpaidReservation.id,
  116. stationName:
  117. mostRecentUnpaidReservation.connector.EquipmentID.StationID.snapshot
  118. .StationName,
  119. address:
  120. mostRecentUnpaidReservation.connector.EquipmentID.StationID.snapshot
  121. .Address
  122. }
  123. });
  124. }
  125. },
  126. {
  127. text: '返回',
  128. onPress: () => {
  129. if (router.canGoBack()) {
  130. router.back();
  131. } else {
  132. router.push('/mainPage');
  133. }
  134. }
  135. }
  136. ],
  137. { cancelable: false }
  138. );
  139. }
  140. } catch (error) {
  141. console.error('Error checking unpaid penalties:', error);
  142. // Handle the error appropriately (e.g., show an error message to the user)
  143. }
  144. };
  145. checkUnpaidPenalties();
  146. }, []);
  147. const handleSendingSize = async (watt) => {
  148. try {
  149. setIsLoading(true);
  150. const wattValue = parseInt(watt.split(' ')[0]);
  151. console.log('wattValue', wattValue);
  152. let size: number;
  153. //make the duration based on watt
  154. switch (wattValue) {
  155. case 20:
  156. size = 25;
  157. break;
  158. case 25:
  159. size = 30;
  160. break;
  161. case 30:
  162. size = 40;
  163. break;
  164. case 40:
  165. size = 45;
  166. break;
  167. default:
  168. console.error('Invalid selectedWatt value');
  169. return; // Exit the function if selectedWatt is invalid
  170. }
  171. const response = await chargeStationService.getReservationWithSize(size);
  172. if (response) {
  173. setDataResponse(response);
  174. // console.log('respoasdasdasdnse', response);
  175. const uniqueDates = new Set();
  176. response.forEach((item) => {
  177. item.timeSlot.forEach((slot) => {
  178. uniqueDates.add(slot.date);
  179. });
  180. });
  181. const availableDates = Array.from(uniqueDates).sort();
  182. setAvailableDate(availableDates);
  183. // console.log('formattedDates', formattedDates);
  184. } else {
  185. console.log('No response from getReservationWithSize');
  186. }
  187. } catch (error) {
  188. console.error('Error in handleSendingSize:', error);
  189. // Handle the error appropriately, maybe show an alert to the user
  190. } finally {
  191. setIsLoading(false);
  192. }
  193. };
  194. //get location
  195. useEffect(() => {
  196. const getCurrentLocation = async () => {
  197. let { status } = await Location.requestForegroundPermissionsAsync();
  198. if (status !== 'granted') {
  199. console.error('Permission to access location was denied');
  200. return;
  201. }
  202. let location = await Location.getLastKnownPositionAsync({});
  203. setCurrentLocation(location);
  204. };
  205. getCurrentLocation();
  206. }, []);
  207. //getDefaultCar
  208. // useEffect(() => {
  209. // const fetchDefaultCar = async () => {
  210. // setIsDefaultCarLoading(true);
  211. // try {
  212. // const response = await chargeStationService.getUserDefaultCars();
  213. // if (response && response.data) {
  214. // setDefaultCar(response.data.id);
  215. // setSelectedCarID(response.data.id);
  216. // setCarCapacitance(response.data.car_type.capacitance);
  217. // setSelectedCar(response.data.id);
  218. // console.log('*******', response.data.car_type.capacitance);
  219. // }
  220. // } catch (error) {
  221. // console.log('Error fetching default car:', error);
  222. // } finally {
  223. // setIsDefaultCarLoading(false);
  224. // }
  225. // };
  226. // fetchDefaultCar();
  227. // }, []);
  228. // const formatDistance = (distanceInMeters: number): string => {
  229. // if (distanceInMeters < 1000) {
  230. // return `${Math.round(distanceInMeters)}米`;
  231. // } else {
  232. // const distanceInKm = distanceInMeters / 1000;
  233. // return `${distanceInKm.toFixed(1)}公里`;
  234. // }
  235. // };
  236. // const getUpcomingReservations = (reservations: any, daysAhead = 3) => {
  237. // const today = new Date();
  238. // const threeDaysLater = new Date(today);
  239. // threeDaysLater.setDate(today.getDate() + daysAhead);
  240. // return reservations
  241. // .filter((reservation) => {
  242. // const reservationDate = new Date(reservation.book_time);
  243. // return reservationDate >= today && reservationDate <= threeDaysLater;
  244. // })
  245. // .sort((a, b) => new Date(a.reservationDate) - new Date(b.reservationDate));
  246. // };
  247. //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.
  248. // const formatReservations = (reservations) => {
  249. // const formattedReservations = {};
  250. // reservations.forEach((reservation) => {
  251. // const bookTime = new Date(reservation.book_time);
  252. // const date = formatDate(bookTime);
  253. // const time = formatTime(bookTime);
  254. // if (!formattedReservations[date]) {
  255. // formattedReservations[date] = [];
  256. // }
  257. // formattedReservations[date].push(time);
  258. // });
  259. // return Object.entries(formattedReservations).map(([date, times]) => ({
  260. // date,
  261. // times
  262. // }));
  263. // };
  264. // const formatDate = (date) => {
  265. // const month = String(date.getMonth() + 1).padStart(2, '0');
  266. // const day = String(date.getDate()).padStart(2, '0');
  267. // return `${month}/${day}`;
  268. // };
  269. // const formatTime = (date) => {
  270. // return date.toTimeString().slice(0, 5);
  271. // };
  272. // useEffect(() => {
  273. // const fetchReservationHistories = async () => {
  274. // try {
  275. // const response = await chargeStationService.fetchReservationHistories();
  276. // if (response) {
  277. // // console.log('response', response);
  278. // // console.log('Reservation histories:', response);
  279. // const upcomingReservations = getUpcomingReservations(response);
  280. // // console.log('upcomingReservations', upcomingReservations);
  281. // const formattedReservations = formatReservations(upcomingReservations);
  282. // // console.log('formattedReservations', formattedReservations);
  283. // setUpcomingReservation(formattedReservations);
  284. // }
  285. // 2;
  286. // } catch (error) {
  287. // console.log(error);
  288. // }
  289. // };
  290. // fetchReservationHistories();
  291. // }, []);
  292. //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.
  293. // useEffect(() => {
  294. // const getDistance = async () => {
  295. // if (currentLocation) {
  296. // try {
  297. // const distance = await calculateDistance(
  298. // Number(params.chargeStationLat),
  299. // Number(params.chargeStationLng),
  300. // currentLocation
  301. // );
  302. // setDistance(formatDistance(distance));
  303. // } catch (error) {
  304. // console.error('Error calculating distance:', error);
  305. // }
  306. // }
  307. // };
  308. // getDistance();
  309. // }, [params.chargeStationLat, params.chargeStationLng, currentLocation]);
  310. useEffect(() => {
  311. const fetchPrice = async () => {
  312. try {
  313. const price = await chargeStationService.fetchChargeStationPrice(chargeStationID);
  314. setPrice(price);
  315. } catch (error) {
  316. console.error('Error fetching price:', error);
  317. }
  318. };
  319. fetchPrice();
  320. }, []);
  321. // function appendImageUrlToCarResult(carData, updatedVehicles) {
  322. // return carData.map((car) => {
  323. // const matchingVehicle = updatedVehicles.find((vehicle) => vehicle.car_type.name === car.car_name);
  324. // if (matchingVehicle) {
  325. // return {
  326. // ...car,
  327. // type_image_url: matchingVehicle.car_type.type_image_url
  328. // };
  329. // }
  330. // return car;
  331. // });
  332. // }
  333. // useEffect(() => {
  334. // setCarLoadingState(true);
  335. // const fetchUserInfoAndCarData = async () => {
  336. // try {
  337. // const fetchedUserInfo = await authenticationService.getUserInfo();
  338. // const userData = fetchedUserInfo?.data;
  339. // // console.log('userData', userData);
  340. // if (userData) {
  341. // setUserID(userData.id);
  342. // const carData = userData.cars.map((car: any) => ({
  343. // car_name: car.name,
  344. // car_capacitance: car.capacitance,
  345. // car_id: car.id,
  346. // isDefault: car.id === userData.defaultCar?.id
  347. // }));
  348. // // console.log('carDarta', carData);
  349. // setCar(carData);
  350. // const carResult = await chargeStationService.getUserCars();
  351. // let updatedVehicles = [...carResult.data];
  352. // // console.log('updatedVehiaacles', updatedVehicles);
  353. // const updatedCarResult = appendImageUrlToCarResult(carData, updatedVehicles);
  354. // setCar(updatedCarResult);
  355. // let updatedCarResultWithProcessedUrl = [...updatedCarResult];
  356. // for (let i = 0; i < updatedCarResultWithProcessedUrl.length; i++) {
  357. // const car = updatedCarResultWithProcessedUrl[i];
  358. // const processedUrl = await chargeStationService.getProcessedImageUrl(car.type_image_url);
  359. // updatedCarResultWithProcessedUrl[i] = {
  360. // ...car,
  361. // processedImageUrl: processedUrl
  362. // };
  363. // }
  364. // // console.log(updatedCarResultWithProcessedUrl);
  365. // setCar(updatedCarResultWithProcessedUrl);
  366. // }
  367. // } catch (error) {
  368. // console.error('Error fetching user info:', error);
  369. // } finally {
  370. // setCarLoadingState(false);
  371. // }
  372. // };
  373. // fetchUserInfoAndCarData();
  374. // }, []);
  375. // useEffect(() => {
  376. // const fetchingAvailableTimeSlots = async () => {
  377. // setIsLoading(true);
  378. // try {
  379. // const fetchedTimeSlots = await chargeStationService.fetchAvailableTimeSlots(
  380. // chargeStationID,
  381. // selectedDate
  382. // );
  383. // const now = new Date();
  384. // const today = `${String(now.getMonth() + 1).padStart(2, '0')}/${String(now.getDate()).padStart(
  385. // 2,
  386. // '0'
  387. // )}`;
  388. // let filteredTimeSlots = fetchedTimeSlots;
  389. // //filter out today's time slots that have already passed
  390. // if (selectedDate === today) {
  391. // const currentHours = now.getHours();
  392. // const currentMinutes = now.getMinutes();
  393. // filteredTimeSlots = fetchedTimeSlots.filter((time) => {
  394. // const [hours, minutes] = time.split(':').map(Number);
  395. // if (hours > currentHours) return true;
  396. // if (hours === currentHours && minutes > currentMinutes) return true;
  397. // return false;
  398. // });
  399. // }
  400. // //filter out time slots that are already fully booked
  401. // const reservedSlotsForDate = upcomingReservations.find((res) => res.date === selectedDate);
  402. // if (reservedSlotsForDate) {
  403. // filteredTimeSlots = filteredTimeSlots.filter((time) => !reservedSlotsForDate.times.includes(time));
  404. // }
  405. // setAvailableTimeSlots(filteredTimeSlots);
  406. // } catch (error) {
  407. // console.error('Error fetching time slots:', error);
  408. // } finally {
  409. // setIsLoading(false);
  410. // }
  411. // };
  412. // if (selectedDate) {
  413. // fetchingAvailableTimeSlots();
  414. // }
  415. // }, [selectedDate]);
  416. // useEffect(() => {
  417. // const fetchConnectorOptions = async () => {
  418. // try {
  419. // const fetchedData = await chargeStationService.fetchSpecificChargeStation(chargeStationID);
  420. // console.log('fetchedDate', fetchedData);
  421. // const dateObject = fetchedData.find((item) => item.date === selectedDate);
  422. // console.log('dateObject', dateObject);
  423. // if (!dateObject) {
  424. // setAvailableConnectorDropdownOptions([]);
  425. // return;
  426. // }
  427. // const rangeObject = dateObject.range.find((range) => range.start === selectedTime);
  428. // console.log('rangeObject', rangeObject);
  429. // if (!rangeObject) {
  430. // setAvailableConnectorDropdownOptions([]);
  431. // return;
  432. // }
  433. // const connectorIDs = rangeObject.connectors
  434. // .filter((connector) => connector.Status === '待机')
  435. // .map((connector) => connector.ConnectorID);
  436. // console.log('connectorIDs', connectorIDs);
  437. // setAvailableConnectorDropdownOptions(connectorIDs);
  438. // } catch (error) {
  439. // console.error('Error fetching charge station data:', error);
  440. // setAvailableConnectorDropdownOptions([]);
  441. // }
  442. // };
  443. // fetchConnectorOptions();
  444. // }, [chargeStationID, selectedDate, selectedTime]);
  445. // old
  446. // const formattedConnectorDropdownOptions = availableConnectorDropdownOptions.map((id, index) => ({
  447. // label: (index + 1).toString(),
  448. // value: id
  449. // }));
  450. const connectorIDToLabelMap = {
  451. '101708240502475001': '1',
  452. '101708240502476001': '2',
  453. '101708240502477001': '3',
  454. '101708240502478001': '4',
  455. '101708240502474001': '5',
  456. '101708240502474002': '6'
  457. };
  458. const connectorIDToLabelMapArray = [
  459. { value: '101708240502475001', label: '1' },
  460. { value: '101708240502476001', label: '2' },
  461. { value: '101708240502477001', label: '3' },
  462. { value: '101708240502478001', label: '4' },
  463. { value: '101708240502474001', label: '5' },
  464. { value: '101708240502474002', label: '6' }
  465. ];
  466. const formatDateString = (dateString: string) => {
  467. const [year, month, day] = dateString.split('-');
  468. return `${month}/${day}`;
  469. };
  470. // useEffect(() => {
  471. // const fetchingAvailableDates = async () => {
  472. // setIsDateLoading(true);
  473. // try {
  474. // const fetchedDates = await chargeStationService.fetchAvailableDates(chargeStationID);
  475. // console.log('fetchedDates', fetchedDates);
  476. // setAvailableDate(fetchedDates);
  477. // console.log(fetchedDates.slice(0, 3));
  478. // } catch (error) {
  479. // console.error('Error fetching available dates:', error);
  480. // } finally {
  481. // setIsDateLoading(false);
  482. // }
  483. // };
  484. // fetchingAvailableDates();
  485. // }, [chargeStationID]);
  486. const handleNavigationPress = () => {
  487. const latitude = chargeStationLat;
  488. const longitude = chargeStationLng;
  489. // console.log('latitude', latitude);
  490. // console.log('longitude', longitude);
  491. const label = encodeURIComponent(chargeStationName);
  492. // Google Maps App URL
  493. const googleMapsUrl = `https://www.google.com/maps/search/?api=1&query=${latitude},${longitude}`;
  494. // Fallback URL for web browser
  495. const webUrl = `https://www.google.com/maps/dir/?api=1&destination=${latitude},${longitude}`;
  496. Linking.canOpenURL(googleMapsUrl)
  497. .then((supported) => {
  498. if (supported) {
  499. Linking.openURL(googleMapsUrl);
  500. } else {
  501. Linking.openURL(webUrl).catch((err) => {
  502. console.error('An error occurred', err);
  503. Alert.alert(
  504. 'Error',
  505. "Unable to open Google Maps. Please make sure it's installed on your device.",
  506. [{ text: 'OK' }],
  507. { cancelable: false }
  508. );
  509. });
  510. }
  511. })
  512. .catch((err) => console.error('An error occurred', err));
  513. };
  514. const handleDateToTimeSlot = (date: string, connectorId: string) => {
  515. // Find the correct connector object
  516. const connectorData = dataResponse.find((item) => item.connector === connectorId);
  517. if (!connectorData) {
  518. console.error(`No data found for connector ${connectorId}`);
  519. setAvailableTimeSlots([]);
  520. return;
  521. }
  522. // Find the timeSlot object for the selected date
  523. const selectedTimeSlot = connectorData.timeSlot.find((slot) => slot.date === date);
  524. if (!selectedTimeSlot) {
  525. console.error(`No time slots found for date ${date}`);
  526. setAvailableTimeSlots([]);
  527. return;
  528. }
  529. const now = new Date();
  530. const selectedDateObj = new Date(date);
  531. const isToday = selectedDateObj.toDateString() === now.toDateString();
  532. let filteredSlots = selectedTimeSlot.availableTime;
  533. if (isToday) {
  534. filteredSlots = filteredSlots.filter((slot) => {
  535. const [hours, minutes] = slot.startTime.split(':').map(Number);
  536. const slotTime = new Date(selectedDateObj);
  537. slotTime.setHours(hours, minutes, 0, 0);
  538. return slotTime > now;
  539. });
  540. }
  541. console.log('Available Time Slots:', filteredSlots);
  542. setAvailableTimeSlots(filteredSlots);
  543. };
  544. const handleConfirmation = (value: any) => {
  545. setModalVisible(true);
  546. const selectedOption = formattedConnectorDropdownOptions.find((option) => option.value === value);
  547. const label = selectedOption ? selectedOption.label : '';
  548. setRouterParams({
  549. pathname: '/bookingConfirmationPage',
  550. params: {
  551. chargeStationName,
  552. chargeStationAddress,
  553. chargeStationID,
  554. connectorID: value,
  555. connectorLabel: label,
  556. userID,
  557. carCapacitance: carCapacitance,
  558. carID: selectedCarID,
  559. carName: selectedCar,
  560. date: selectedDate,
  561. bookTime: selectedTime,
  562. chargingMethod: stopChargingUponBatteryFull ? 'stopChargingUponBatteryFull' : 'chargingBasedOnWatt',
  563. chargingWatt: selectedWatt || '',
  564. price: price
  565. }
  566. });
  567. };
  568. const handleModalConfirm = () => {
  569. setModalVisible(false);
  570. if (routerParams) {
  571. console.log('routerParams', routerParams);
  572. router.push(routerParams);
  573. }
  574. };
  575. return (
  576. <SafeAreaView
  577. style={{
  578. flex: 1,
  579. backgroundColor: 'white'
  580. }}
  581. edges={['right', 'top', 'left']}
  582. >
  583. <ScrollView className="flex-1 bg-white" showsVerticalScrollIndicator={false}>
  584. <View className="pb-4 ">
  585. <View className="ml-[5%] pt-8">
  586. <Pressable
  587. style={{ alignSelf: 'flex-start' }}
  588. onPress={() => {
  589. if (router.canGoBack()) {
  590. router.back();
  591. } else {
  592. router.replace('./');
  593. }
  594. }}
  595. >
  596. <PreviousPageBlackSvg />
  597. </Pressable>
  598. <Text className="text-3xl mt-8">{chargeStationName}</Text>
  599. <View className="flex-column">
  600. <View className="flex-row justify-between items-center mr-[5%]">
  601. <Text className="text-base" style={styles.grayColor}>
  602. {chargeStationAddress}
  603. </Text>
  604. <NormalButton
  605. title={
  606. <View className="flex-row items-center justify-center text-center space-x-1">
  607. <DirectionLogoSvg />
  608. <Text className="text-base">路線</Text>
  609. </View>
  610. }
  611. // onPress={() => console.log('路線')}
  612. onPress={handleNavigationPress}
  613. extendedStyle={{
  614. backgroundColor: '#E3F2F8',
  615. borderRadius: 61,
  616. paddingHorizontal: 20,
  617. paddingVertical: 8
  618. }}
  619. />
  620. </View>
  621. <View className="flex-row space-x-2 items-center">
  622. <CheckMarkLogoSvg />
  623. <Text>Walk-In</Text>
  624. {/* <Text>{distance}</Text> */}
  625. </View>
  626. </View>
  627. </View>
  628. </View>
  629. <View>
  630. {/* {selectedCar !== '' ? (
  631. <>
  632. <Pressable
  633. onPress={() => {
  634. setSelectedCar('');
  635. setSelectedWatt('');
  636. setOpenDrawer(0);
  637. setSelectedDrawer(0);
  638. setSelectedDuration('');
  639. setChargingBasedOnWatt(false);
  640. setStopChargingUponBatteryFull(false);
  641. setSelectedDate('');
  642. setSelectedTime('');
  643. }}
  644. >
  645. <View className="mx-[5%]">
  646. <View className="flex-row items-center pt-4">
  647. <Text className="text-lg pr-2 text-[#34667c]">選擇充電車輛</Text>
  648. <CheckMarkLogoSvg />
  649. </View>
  650. <Text className="text-lg pb-4">{selectedCar}</Text>
  651. </View>
  652. </Pressable>
  653. </>
  654. ) : (
  655. <AccordionItem
  656. title="選擇充電車輛"
  657. isOpen={openDrawer === 0}
  658. onToggle={() => toggleDrawer(0)}
  659. isSelected={selectedDrawer === 0}
  660. >
  661. {carLoadingState ? (
  662. <View>
  663. <ActivityIndicator color="#34657b" />
  664. </View>
  665. ) : (
  666. <ScrollView
  667. horizontal={true}
  668. contentContainerStyle={{
  669. alignItems: 'center',
  670. flexDirection: 'row',
  671. marginVertical: 8
  672. }}
  673. className="space-x-2 "
  674. >
  675. {car
  676. .sort((a, b) => (b.isDefault ? 1 : 0) - (a.isDefault ? 1 : 0))
  677. .map((car, index) => (
  678. <ChooseCarForChargingRow
  679. onPress={() => {
  680. setSelectedCar(car.car_name);
  681. setSelectedCarID(car.car_id);
  682. setCarCapacitance(car.car_capacitance);
  683. setSelectedDrawer(1);
  684. setOpenDrawer(1);
  685. }}
  686. image={car.processedImageUrl}
  687. key={`${car.car_name}+${index}`}
  688. VehicleName={car.car_name}
  689. isDefault={car.isDefault}
  690. />
  691. ))}
  692. </ScrollView>
  693. )}
  694. </AccordionItem>
  695. )} */}
  696. {stopChargingUponBatteryFull === true || selectedWatt !== '' ? (
  697. <Pressable
  698. onPress={() => {
  699. setSelectedDuration('');
  700. setChargingBasedOnWatt(false);
  701. setStopChargingUponBatteryFull(false);
  702. setSelectedTime('');
  703. setSelectedDate('');
  704. setSelectedWatt('');
  705. setOpenDrawer(1);
  706. setSelectedDrawer(1);
  707. setSelectedChargingGun('');
  708. }}
  709. >
  710. <View className="mx-[5%] ">
  711. <View className="flex-row items-center pt-4">
  712. <Text className="text-lg pr-2 text-[#34667c]">選擇充電方案</Text>
  713. <CheckMarkLogoSvg />
  714. </View>
  715. <Text className="text-lg pb-4">
  716. {selectedWatt !== '' ? `按每道電 - ${selectedWatt.split('~')[0]}` : '充滿停機'}
  717. </Text>
  718. </View>
  719. </Pressable>
  720. ) : (
  721. <AccordionItem
  722. title="選擇充電方案"
  723. isOpen={openDrawer === 1}
  724. onToggle={() => {}}
  725. isSelected={selectedDrawer === 1}
  726. >
  727. <View className="flex-row justify-between mt-2 mb-3">
  728. <Pressable
  729. className={`border rounded-lg border-[#34667c] w-[47%] items-center bg-white ${
  730. chargingBasedOnWatt ? 'bg-[#34667c] ' : ''
  731. }`}
  732. onPress={() => {
  733. setChargingBasedOnWatt(!chargingBasedOnWatt);
  734. setStopChargingUponBatteryFull(false);
  735. }}
  736. >
  737. <Text
  738. className={`text-base p-2 text-[#34667c] ${
  739. chargingBasedOnWatt ? ' text-white' : 'text-[#34667c]'
  740. }`}
  741. >
  742. 按每度電
  743. </Text>
  744. </Pressable>
  745. {/* <Pressable
  746. onPress={() => {
  747. setStopChargingUponBatteryFull(!stopChargingUponBatteryFull);
  748. setChargingBasedOnWatt(false);
  749. setSelectedDrawer(2);
  750. setOpenDrawer(2);
  751. }}
  752. className={`border rounded-lg border-[#34667c] w-[47%] items-center bg-white ${
  753. stopChargingUponBatteryFull ? ' bg-[#34667c]' : ''
  754. }`}
  755. >
  756. <Text
  757. className={`text-base p-2 text-[#34667c] ${
  758. stopChargingUponBatteryFull ? ' text-white' : 'text-[#34667c]'
  759. }`}
  760. >
  761. 充滿停機
  762. </Text>
  763. </Pressable> */}
  764. </View>
  765. {chargingBasedOnWatt === true && (
  766. <View className="flex-row w-full justify-between mb-3">
  767. {['20 kWh~25mins', '25 kWh~30mins', '30 kWh~40mins', '40 kWh~45mins'].map(
  768. (watt) => (
  769. <Pressable
  770. key={watt}
  771. className={`${
  772. selectedWatt === watt ? 'bg-[#34667c] ' : 'bg-white'
  773. } border border-[#34667c] rounded-lg w-[22%] items-center`}
  774. onPress={() => {
  775. setSelectedWatt(watt);
  776. setOpenDrawer(2);
  777. setSelectedDrawer(2);
  778. handleSendingSize(watt);
  779. console.log('selectedWatt', selectedWatt);
  780. console.log('watt', watt);
  781. }}
  782. >
  783. <Text
  784. className={`text-base pt-2 pl-2 pr-2 ${
  785. selectedWatt === watt ? 'text-white' : 'text-[#34667c]'
  786. } `}
  787. >
  788. {watt.split('~')[0]}
  789. </Text>
  790. <Text className="text-xs pt-0 pb-1 text-[#666666]">
  791. {watt.split('~')[1]}
  792. </Text>
  793. </Pressable>
  794. )
  795. )}
  796. </View>
  797. )}
  798. </AccordionItem>
  799. )}
  800. {/* select gun */}
  801. {/* select gun */}
  802. <View className="">
  803. {selectedChargingGun !== '' ? (
  804. <Pressable
  805. onPress={() => {
  806. setSelectedChargingGun('');
  807. setOpenDrawer(2);
  808. setSelectedDrawer(2);
  809. }}
  810. >
  811. <View className="mx-[5%]">
  812. <View className="flex-row items-center pt-4">
  813. <Text className="text-lg pr-2 text-[#34667c]">選擇充電座</Text>
  814. <CheckMarkLogoSvg />
  815. </View>
  816. <View className="text-lg pb-4 flex flex-row items-center">
  817. <Text className="text-[#34667c] font-[600] text-2xl pr-2">
  818. {connectorIDToLabelMap[selectedChargingGun]}
  819. </Text>
  820. <Text className="text-lg">號充電座</Text>
  821. </View>
  822. </View>
  823. </Pressable>
  824. ) : (
  825. <AccordionItem
  826. title="選擇充電座"
  827. isOpen={openDrawer === 2}
  828. onToggle={() => {
  829. if (selectedWatt) {
  830. toggleDrawer(2);
  831. }
  832. }}
  833. isSelected={selectedDrawer === 2}
  834. >
  835. {selectedWatt !== '' ? (
  836. <View className="">
  837. <DropdownSelect
  838. dropdownOptions={connectorIDToLabelMapArray}
  839. placeholder={'選擇充電座號碼'}
  840. onSelect={(value) => {
  841. setSelectedChargingGun(value);
  842. setSelectedDrawer(3);
  843. setOpenDrawer(3);
  844. }}
  845. extendedStyle={{
  846. borderColor: '#34667c',
  847. marginTop: 4,
  848. padding: 12
  849. }}
  850. />
  851. <Image
  852. style={{
  853. width: layoutWidth * 0.9,
  854. height: layoutHeight
  855. }}
  856. resizeMode="contain"
  857. source={require('../../assets/floorPlan1.png')}
  858. />
  859. </View>
  860. ) : (
  861. <Text className="text-base text-gray-500 py-2">請先選擇充電方案</Text>
  862. )}
  863. </AccordionItem>
  864. )}
  865. </View>
  866. <Modal
  867. isVisible={isModalVisible}
  868. // onBackdropPress={() => setModalVisible(false)}
  869. backdropOpacity={0.5}
  870. animationIn="fadeIn"
  871. animationOut="fadeOut"
  872. >
  873. <View style={styles.modalContent}>
  874. <Text className="text-2xl font-[500] text-[#34667c] mb-6">
  875. 已選擇日期時間: {formatDateString(selectedDate)} - {selectedTime}
  876. </Text>
  877. <Text style={styles.modalText} className="text-[#34667c]">
  878. 若客戶逾時超過15分鐘,系統將視作自動放棄預約,客戶需要重新預約一次。
  879. 本公司有權保留全數費用,恕不退還。按下確認代表您已閱讀並同意上述條款。
  880. </Text>
  881. <NormalButton
  882. title={<Text className="text-white">我確認</Text>}
  883. onPress={handleModalConfirm}
  884. extendedStyle={styles.confirmButton}
  885. />
  886. </View>
  887. </Modal>
  888. {selectedDate !== '' && selectedTime !== '' ? (
  889. <>
  890. <Pressable
  891. onPress={() => {
  892. setOpenDrawer(3);
  893. setSelectedDrawer(3);
  894. setSelectedDate('');
  895. setSelectedTime('');
  896. }}
  897. >
  898. <View className="mx-[5%] ">
  899. <View className="flex-row items-center pt-4">
  900. <Text className="text-lg pr-2 text-[#34667c]">選擇日期</Text>
  901. <CheckMarkLogoSvg />
  902. </View>
  903. <Text className="text-lg pb-4">
  904. {formatDateString(selectedDate)} - {selectedTime}
  905. </Text>
  906. </View>
  907. </Pressable>
  908. </>
  909. ) : (
  910. <AccordionItem
  911. title="選擇日期 (月/日)"
  912. isOpen={openDrawer === 3}
  913. onToggle={() => {
  914. if (stopChargingUponBatteryFull !== false || selectedDuration !== '') {
  915. toggleDrawer(3);
  916. }
  917. }}
  918. isSelected={selectedDrawer === 3}
  919. >
  920. {isDateLoading ? (
  921. <View className="flex-1 items-center justify-center py-4">
  922. <ActivityIndicator size="large" color="#34667c" />
  923. </View>
  924. ) : (
  925. <View className="flex-row w-full flex-wrap mb-1 ">
  926. {availableDate.map((date) => (
  927. <Pressable
  928. key={date}
  929. className={`${
  930. selectedDate === date ? 'bg-[#34667c] ' : 'bg-white'
  931. } border border-[#34667c] rounded-lg w-[22%] items-center mt-1 mr-1 mb-1`}
  932. onPress={() => {
  933. setSelectedDate(date);
  934. handleDateToTimeSlot(date, selectedChargingGun);
  935. }}
  936. >
  937. <Text
  938. className={`text-base p-2 ${
  939. selectedDate === date ? 'text-white' : 'text-[#34667c]'
  940. } `}
  941. >
  942. {formatDateString(date)}
  943. </Text>
  944. </Pressable>
  945. ))}
  946. </View>
  947. )}
  948. {selectedDate !== '' && (
  949. <>
  950. <Text className="text-lg pr-2 ">選擇時間</Text>
  951. {isLoading ? (
  952. <View className="flex-1 mb-2">
  953. <ActivityIndicator />
  954. </View>
  955. ) : (
  956. <View className="flex-row w-full mb-3 flex-wrap my-2 ">
  957. {availableTimeSlots.map((slot, index) => (
  958. <Pressable
  959. key={index}
  960. className={`${
  961. selectedTime === slot.startTime ? 'bg-[#34667c] ' : 'bg-white'
  962. } border border-[#34667c] mr-2 rounded-lg w-[22%] items-center mb-2`}
  963. onPress={() => {
  964. setSelectedTime(slot.startTime);
  965. setRouterParams({
  966. pathname: '/bookingConfirmationPage',
  967. params: {
  968. chargeStationName,
  969. chargeStationAddress,
  970. chargeStationID,
  971. connectorID: selectedChargingGun,
  972. connectorLabel:
  973. connectorIDToLabelMap[selectedChargingGun],
  974. userID,
  975. carCapacitance: carCapacitance,
  976. carID: selectedCarID,
  977. carName: selectedCar,
  978. date: selectedDate,
  979. bookTime: slot.startTime,
  980. endTime: slot.endTime,
  981. chargingMethod: stopChargingUponBatteryFull
  982. ? 'stopChargingUponBatteryFull'
  983. : 'chargingBasedOnWatt',
  984. chargingWatt: selectedWatt || '',
  985. price: price
  986. }
  987. });
  988. setModalVisible(true);
  989. }}
  990. >
  991. <Text
  992. className={`text-base p-2 ${
  993. selectedTime === slot.startTime
  994. ? 'text-white'
  995. : 'text-[#34667c]'
  996. } `}
  997. >
  998. {slot.startTime}
  999. </Text>
  1000. </Pressable>
  1001. ))}
  1002. </View>
  1003. )}
  1004. </>
  1005. )}
  1006. </AccordionItem>
  1007. )}
  1008. {/* <View className="">
  1009. <AccordionItem
  1010. title="選擇充電座"
  1011. isOpen={openDrawer === 3}
  1012. onToggle={() => {
  1013. if (selectedTime) {
  1014. // toggleDrawer(3);
  1015. }
  1016. }}
  1017. isSelected={selectedDrawer === 3}
  1018. >
  1019. <View className="">
  1020. <DropdownSelect
  1021. dropdownOptions={formattedConnectorDropdownOptions}
  1022. placeholder={'選擇充電座號碼'}
  1023. onSelect={(value) => {
  1024. setSelectedChargingGun(value);
  1025. handleConfirmation(value);
  1026. }}
  1027. extendedStyle={{
  1028. borderColor: '#34667c',
  1029. marginTop: 4,
  1030. padding: 12
  1031. }}
  1032. />
  1033. <Image
  1034. style={{
  1035. width: layoutWidth * 0.9,
  1036. height: layoutHeight
  1037. }}
  1038. resizeMode="contain"
  1039. source={require('../../assets/floorPlan1.png')}
  1040. />
  1041. <Modal
  1042. isVisible={isModalVisible}
  1043. // onBackdropPress={() => setModalVisible(false)}
  1044. backdropOpacity={0.5}
  1045. animationIn="fadeIn"
  1046. animationOut="fadeOut"
  1047. >
  1048. <View style={styles.modalContent}>
  1049. <Text style={styles.modalText}>
  1050. 若客戶逾時超過15分鐘,系統將視作自動放棄預約,客戶需要重新預約一次。
  1051. 本公司有權保留全數費用,恕不退還。按下確認代表您已閱讀並同意上述條款。
  1052. </Text>
  1053. <NormalButton
  1054. title={<Text className="text-white">我確認</Text>}
  1055. onPress={handleModalConfirm}
  1056. extendedStyle={styles.confirmButton}
  1057. />
  1058. </View>
  1059. </Modal>
  1060. </View>
  1061. </AccordionItem>
  1062. </View> */}
  1063. </View>
  1064. <View className="mx-[5%] flex-1">
  1065. <Text className="text-xl pb-2 mt-6" style={styles.text}>
  1066. 充電站資訊
  1067. </Text>
  1068. <View className="h-[250px]">
  1069. <ChargingStationTabView titles={['預約充電事項', '其他']} />
  1070. </View>
  1071. </View>
  1072. </ScrollView>
  1073. </SafeAreaView>
  1074. );
  1075. };
  1076. export default MakingBookingPageComponent;
  1077. const styles = StyleSheet.create({
  1078. grayColor: {
  1079. color: '#888888'
  1080. },
  1081. topLeftTriangle: {
  1082. width: 0,
  1083. height: 0,
  1084. borderLeftWidth: 50,
  1085. borderBottomWidth: 50,
  1086. borderLeftColor: '#02677D',
  1087. borderBottomColor: 'transparent',
  1088. position: 'absolute',
  1089. top: 0,
  1090. left: 0
  1091. },
  1092. modalContent: {
  1093. backgroundColor: 'white',
  1094. padding: 22,
  1095. justifyContent: 'center',
  1096. alignItems: 'center',
  1097. borderRadius: 4,
  1098. borderColor: 'rgba(0, 0, 0, 0.1)'
  1099. },
  1100. modalText: {
  1101. fontSize: 18,
  1102. marginBottom: 12,
  1103. textAlign: 'center'
  1104. },
  1105. confirmButton: {
  1106. backgroundColor: '#34667c',
  1107. paddingHorizontal: 30,
  1108. paddingVertical: 10,
  1109. borderRadius: 5
  1110. },
  1111. text: {
  1112. fontWeight: 300,
  1113. color: '#000000'
  1114. }
  1115. });