makingBookingPageComponent.tsx 57 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188
  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. setAvailableTimeSlots(filteredSlots);
  542. };
  543. const handleConfirmation = (value: any) => {
  544. setModalVisible(true);
  545. const selectedOption = formattedConnectorDropdownOptions.find((option) => option.value === value);
  546. const label = selectedOption ? selectedOption.label : '';
  547. setRouterParams({
  548. pathname: '/bookingConfirmationPage',
  549. params: {
  550. chargeStationName,
  551. chargeStationAddress,
  552. chargeStationID,
  553. connectorID: value,
  554. connectorLabel: label,
  555. userID,
  556. carCapacitance: carCapacitance,
  557. carID: selectedCarID,
  558. carName: selectedCar,
  559. date: selectedDate,
  560. bookTime: selectedTime,
  561. chargingMethod: stopChargingUponBatteryFull ? 'stopChargingUponBatteryFull' : 'chargingBasedOnWatt',
  562. chargingWatt: selectedWatt || '',
  563. price: price
  564. }
  565. });
  566. };
  567. const handleModalConfirm = () => {
  568. setModalVisible(false);
  569. if (routerParams) {
  570. router.push(routerParams);
  571. }
  572. };
  573. return (
  574. <SafeAreaView
  575. style={{
  576. flex: 1,
  577. backgroundColor: 'white'
  578. }}
  579. edges={['right', 'top', 'left']}
  580. >
  581. <ScrollView className="flex-1 bg-white" showsVerticalScrollIndicator={false}>
  582. <View className="pb-4 ">
  583. <View className="ml-[5%] pt-8">
  584. <Pressable
  585. style={{ alignSelf: 'flex-start' }}
  586. onPress={() => {
  587. if (router.canGoBack()) {
  588. router.back();
  589. } else {
  590. router.replace('./');
  591. }
  592. }}
  593. >
  594. <PreviousPageBlackSvg />
  595. </Pressable>
  596. <Text className="text-3xl mt-8">{chargeStationName}</Text>
  597. <View className="flex-column">
  598. <View className="flex-row justify-between items-center mr-[5%]">
  599. <Text className="text-base" style={styles.grayColor}>
  600. {chargeStationAddress}
  601. </Text>
  602. <NormalButton
  603. title={
  604. <View className="flex-row items-center justify-center text-center space-x-1">
  605. <DirectionLogoSvg />
  606. <Text className="text-base">路線</Text>
  607. </View>
  608. }
  609. // onPress={() => console.log('路線')}
  610. onPress={handleNavigationPress}
  611. extendedStyle={{
  612. backgroundColor: '#E3F2F8',
  613. borderRadius: 61,
  614. paddingHorizontal: 20,
  615. paddingVertical: 8
  616. }}
  617. />
  618. </View>
  619. <View className="flex-row space-x-2 items-center">
  620. <CheckMarkLogoSvg />
  621. <Text>Walk-In</Text>
  622. {/* <Text>{distance}</Text> */}
  623. </View>
  624. </View>
  625. </View>
  626. </View>
  627. <View>
  628. {/* {selectedCar !== '' ? (
  629. <>
  630. <Pressable
  631. onPress={() => {
  632. setSelectedCar('');
  633. setSelectedWatt('');
  634. setOpenDrawer(0);
  635. setSelectedDrawer(0);
  636. setSelectedDuration('');
  637. setChargingBasedOnWatt(false);
  638. setStopChargingUponBatteryFull(false);
  639. setSelectedDate('');
  640. setSelectedTime('');
  641. }}
  642. >
  643. <View className="mx-[5%]">
  644. <View className="flex-row items-center pt-4">
  645. <Text className="text-lg pr-2 text-[#34667c]">選擇充電車輛</Text>
  646. <CheckMarkLogoSvg />
  647. </View>
  648. <Text className="text-lg pb-4">{selectedCar}</Text>
  649. </View>
  650. </Pressable>
  651. </>
  652. ) : (
  653. <AccordionItem
  654. title="選擇充電車輛"
  655. isOpen={openDrawer === 0}
  656. onToggle={() => toggleDrawer(0)}
  657. isSelected={selectedDrawer === 0}
  658. >
  659. {carLoadingState ? (
  660. <View>
  661. <ActivityIndicator color="#34657b" />
  662. </View>
  663. ) : (
  664. <ScrollView
  665. horizontal={true}
  666. contentContainerStyle={{
  667. alignItems: 'center',
  668. flexDirection: 'row',
  669. marginVertical: 8
  670. }}
  671. className="space-x-2 "
  672. >
  673. {car
  674. .sort((a, b) => (b.isDefault ? 1 : 0) - (a.isDefault ? 1 : 0))
  675. .map((car, index) => (
  676. <ChooseCarForChargingRow
  677. onPress={() => {
  678. setSelectedCar(car.car_name);
  679. setSelectedCarID(car.car_id);
  680. setCarCapacitance(car.car_capacitance);
  681. setSelectedDrawer(1);
  682. setOpenDrawer(1);
  683. }}
  684. image={car.processedImageUrl}
  685. key={`${car.car_name}+${index}`}
  686. VehicleName={car.car_name}
  687. isDefault={car.isDefault}
  688. />
  689. ))}
  690. </ScrollView>
  691. )}
  692. </AccordionItem>
  693. )} */}
  694. {stopChargingUponBatteryFull === true || selectedWatt !== '' ? (
  695. <Pressable
  696. onPress={() => {
  697. setSelectedDuration('');
  698. setChargingBasedOnWatt(false);
  699. setStopChargingUponBatteryFull(false);
  700. setSelectedTime('');
  701. setSelectedDate('');
  702. setSelectedWatt('');
  703. setOpenDrawer(1);
  704. setSelectedDrawer(1);
  705. setSelectedChargingGun('');
  706. }}
  707. >
  708. <View className="mx-[5%] ">
  709. <View className="flex-row items-center pt-4">
  710. <Text className="text-lg pr-2 text-[#34667c]">選擇充電方案</Text>
  711. <CheckMarkLogoSvg />
  712. </View>
  713. <Text className="text-lg pb-4">
  714. {selectedWatt !== '' ? `按每道電 - ${selectedWatt.split('~')[0]}` : '充滿停機'}
  715. </Text>
  716. </View>
  717. </Pressable>
  718. ) : (
  719. <AccordionItem
  720. title="選擇充電方案"
  721. isOpen={openDrawer === 1}
  722. onToggle={() => {}}
  723. isSelected={selectedDrawer === 1}
  724. >
  725. <View className="flex-row justify-between mt-2 mb-3">
  726. <Pressable
  727. className={`border rounded-lg border-[#34667c] w-[47%] items-center bg-white ${
  728. chargingBasedOnWatt ? 'bg-[#34667c] ' : ''
  729. }`}
  730. onPress={() => {
  731. setChargingBasedOnWatt(!chargingBasedOnWatt);
  732. setStopChargingUponBatteryFull(false);
  733. }}
  734. >
  735. <Text
  736. className={`text-base p-2 text-[#34667c] ${
  737. chargingBasedOnWatt ? ' text-white' : 'text-[#34667c]'
  738. }`}
  739. >
  740. 按每度電
  741. </Text>
  742. </Pressable>
  743. {/* <Pressable
  744. onPress={() => {
  745. setStopChargingUponBatteryFull(!stopChargingUponBatteryFull);
  746. setChargingBasedOnWatt(false);
  747. setSelectedDrawer(2);
  748. setOpenDrawer(2);
  749. }}
  750. className={`border rounded-lg border-[#34667c] w-[47%] items-center bg-white ${
  751. stopChargingUponBatteryFull ? ' bg-[#34667c]' : ''
  752. }`}
  753. >
  754. <Text
  755. className={`text-base p-2 text-[#34667c] ${
  756. stopChargingUponBatteryFull ? ' text-white' : 'text-[#34667c]'
  757. }`}
  758. >
  759. 充滿停機
  760. </Text>
  761. </Pressable> */}
  762. </View>
  763. {chargingBasedOnWatt === true && (
  764. <View className="flex-row w-full justify-between mb-3">
  765. {['20 kWh~25mins', '25 kWh~30mins', '30 kWh~40mins', '40 kWh~45mins'].map(
  766. (watt) => (
  767. <Pressable
  768. key={watt}
  769. className={`${
  770. selectedWatt === watt ? 'bg-[#34667c] ' : 'bg-white'
  771. } border border-[#34667c] rounded-lg w-[22%] items-center`}
  772. onPress={() => {
  773. setSelectedWatt(watt);
  774. setOpenDrawer(2);
  775. setSelectedDrawer(2);
  776. handleSendingSize(watt);
  777. }}
  778. >
  779. <Text
  780. className={`text-base pt-2 pl-2 pr-2 ${
  781. selectedWatt === watt ? 'text-white' : 'text-[#34667c]'
  782. } `}
  783. >
  784. {watt.split('~')[0]}
  785. </Text>
  786. <Text className="text-xs pt-0 pb-1 text-[#666666]">
  787. {watt.split('~')[1]}
  788. </Text>
  789. </Pressable>
  790. )
  791. )}
  792. </View>
  793. )}
  794. </AccordionItem>
  795. )}
  796. {/* select gun */}
  797. {/* select gun */}
  798. <View className="">
  799. {selectedChargingGun !== '' ? (
  800. <Pressable
  801. onPress={() => {
  802. setSelectedChargingGun('');
  803. setOpenDrawer(2);
  804. setSelectedDrawer(2);
  805. }}
  806. >
  807. <View className="mx-[5%]">
  808. <View className="flex-row items-center pt-4">
  809. <Text className="text-lg pr-2 text-[#34667c]">選擇充電座</Text>
  810. <CheckMarkLogoSvg />
  811. </View>
  812. <View className="text-lg pb-4 flex flex-row items-center">
  813. <Text className="text-[#34667c] font-[600] text-2xl pr-2">
  814. {connectorIDToLabelMap[selectedChargingGun]}
  815. </Text>
  816. <Text className="text-lg">號充電座</Text>
  817. </View>
  818. </View>
  819. </Pressable>
  820. ) : (
  821. <AccordionItem
  822. title="選擇充電座"
  823. isOpen={openDrawer === 2}
  824. onToggle={() => {
  825. if (selectedWatt) {
  826. toggleDrawer(2);
  827. }
  828. }}
  829. isSelected={selectedDrawer === 2}
  830. >
  831. {selectedWatt !== '' ? (
  832. <View className="">
  833. <DropdownSelect
  834. dropdownOptions={connectorIDToLabelMapArray}
  835. placeholder={'選擇充電座號碼'}
  836. onSelect={(value) => {
  837. setSelectedChargingGun(value);
  838. setSelectedDrawer(3);
  839. setOpenDrawer(3);
  840. }}
  841. extendedStyle={{
  842. borderColor: '#34667c',
  843. marginTop: 4,
  844. padding: 12
  845. }}
  846. />
  847. <Image
  848. style={{
  849. width: layoutWidth * 0.9,
  850. height: layoutHeight
  851. }}
  852. resizeMode="contain"
  853. source={require('../../assets/floorPlan1.png')}
  854. />
  855. </View>
  856. ) : (
  857. <Text className="text-base text-gray-500 py-2">請先選擇充電方案</Text>
  858. )}
  859. </AccordionItem>
  860. )}
  861. </View>
  862. <Modal
  863. isVisible={isModalVisible}
  864. // onBackdropPress={() => setModalVisible(false)}
  865. backdropOpacity={0.5}
  866. animationIn="fadeIn"
  867. animationOut="fadeOut"
  868. >
  869. <View style={styles.modalContent}>
  870. <Text className="text-2xl font-[500] text-[#34667c] mb-6">
  871. 已選擇日期時間: {formatDateString(selectedDate)} - {selectedTime}
  872. </Text>
  873. <Text style={styles.modalText} className="text-[#34667c]">
  874. 若客戶逾時超過15分鐘,系統將視作自動放棄預約,客戶需要重新預約一次。
  875. 本公司有權保留全數費用,恕不退還。按下確認代表您已閱讀並同意上述條款。
  876. </Text>
  877. <NormalButton
  878. title={<Text className="text-white">我確認</Text>}
  879. onPress={handleModalConfirm}
  880. extendedStyle={styles.confirmButton}
  881. />
  882. </View>
  883. </Modal>
  884. {selectedDate !== '' && selectedTime !== '' ? (
  885. <>
  886. <Pressable
  887. onPress={() => {
  888. setOpenDrawer(3);
  889. setSelectedDrawer(3);
  890. setSelectedDate('');
  891. setSelectedTime('');
  892. }}
  893. >
  894. <View className="mx-[5%] ">
  895. <View className="flex-row items-center pt-4">
  896. <Text className="text-lg pr-2 text-[#34667c]">選擇日期</Text>
  897. <CheckMarkLogoSvg />
  898. </View>
  899. <Text className="text-lg pb-4">
  900. {formatDateString(selectedDate)} - {selectedTime}
  901. </Text>
  902. </View>
  903. </Pressable>
  904. </>
  905. ) : (
  906. <AccordionItem
  907. title="選擇日期 (月/日)"
  908. isOpen={openDrawer === 3}
  909. onToggle={() => {
  910. if (stopChargingUponBatteryFull !== false || selectedDuration !== '') {
  911. toggleDrawer(3);
  912. }
  913. }}
  914. isSelected={selectedDrawer === 3}
  915. >
  916. {isDateLoading ? (
  917. <View className="flex-1 items-center justify-center py-4">
  918. <ActivityIndicator size="large" color="#34667c" />
  919. </View>
  920. ) : (
  921. <View className="flex-row w-full flex-wrap mb-1 ">
  922. {availableDate.map((date) => (
  923. <Pressable
  924. key={date}
  925. className={`${
  926. selectedDate === date ? 'bg-[#34667c] ' : 'bg-white'
  927. } border border-[#34667c] rounded-lg w-[22%] items-center mt-1 mr-1 mb-1`}
  928. onPress={() => {
  929. setSelectedDate(date);
  930. handleDateToTimeSlot(date, selectedChargingGun);
  931. }}
  932. >
  933. <Text
  934. className={`text-base p-2 ${
  935. selectedDate === date ? 'text-white' : 'text-[#34667c]'
  936. } `}
  937. >
  938. {formatDateString(date)}
  939. </Text>
  940. </Pressable>
  941. ))}
  942. </View>
  943. )}
  944. {selectedDate !== '' && (
  945. <>
  946. <Text className="text-lg pr-2 ">選擇時間</Text>
  947. {isLoading ? (
  948. <View className="flex-1 mb-2">
  949. <ActivityIndicator />
  950. </View>
  951. ) : (
  952. <View className="flex-row w-full mb-3 flex-wrap my-2 ">
  953. {availableTimeSlots.map((slot, index) => (
  954. <Pressable
  955. key={index}
  956. className={`${
  957. selectedTime === slot.startTime ? 'bg-[#34667c] ' : 'bg-white'
  958. } border border-[#34667c] mr-2 rounded-lg w-[22%] items-center mb-2`}
  959. onPress={() => {
  960. setSelectedTime(slot.startTime);
  961. setRouterParams({
  962. pathname: '/bookingConfirmationPage',
  963. params: {
  964. chargeStationName,
  965. chargeStationAddress,
  966. chargeStationID,
  967. connectorID: selectedChargingGun,
  968. connectorLabel:
  969. connectorIDToLabelMap[selectedChargingGun],
  970. userID,
  971. carCapacitance: carCapacitance,
  972. carID: selectedCarID,
  973. carName: selectedCar,
  974. date: selectedDate,
  975. bookTime: slot.startTime,
  976. endTime: slot.endTime,
  977. chargingMethod: stopChargingUponBatteryFull
  978. ? 'stopChargingUponBatteryFull'
  979. : 'chargingBasedOnWatt',
  980. chargingWatt: selectedWatt || '',
  981. price: price
  982. }
  983. });
  984. setModalVisible(true);
  985. }}
  986. >
  987. <Text
  988. className={`text-base p-2 ${
  989. selectedTime === slot.startTime
  990. ? 'text-white'
  991. : 'text-[#34667c]'
  992. } `}
  993. >
  994. {slot.startTime}
  995. </Text>
  996. </Pressable>
  997. ))}
  998. </View>
  999. )}
  1000. </>
  1001. )}
  1002. </AccordionItem>
  1003. )}
  1004. {/* <View className="">
  1005. <AccordionItem
  1006. title="選擇充電座"
  1007. isOpen={openDrawer === 3}
  1008. onToggle={() => {
  1009. if (selectedTime) {
  1010. // toggleDrawer(3);
  1011. }
  1012. }}
  1013. isSelected={selectedDrawer === 3}
  1014. >
  1015. <View className="">
  1016. <DropdownSelect
  1017. dropdownOptions={formattedConnectorDropdownOptions}
  1018. placeholder={'選擇充電座號碼'}
  1019. onSelect={(value) => {
  1020. setSelectedChargingGun(value);
  1021. handleConfirmation(value);
  1022. }}
  1023. extendedStyle={{
  1024. borderColor: '#34667c',
  1025. marginTop: 4,
  1026. padding: 12
  1027. }}
  1028. />
  1029. <Image
  1030. style={{
  1031. width: layoutWidth * 0.9,
  1032. height: layoutHeight
  1033. }}
  1034. resizeMode="contain"
  1035. source={require('../../assets/floorPlan1.png')}
  1036. />
  1037. <Modal
  1038. isVisible={isModalVisible}
  1039. // onBackdropPress={() => setModalVisible(false)}
  1040. backdropOpacity={0.5}
  1041. animationIn="fadeIn"
  1042. animationOut="fadeOut"
  1043. >
  1044. <View style={styles.modalContent}>
  1045. <Text style={styles.modalText}>
  1046. 若客戶逾時超過15分鐘,系統將視作自動放棄預約,客戶需要重新預約一次。
  1047. 本公司有權保留全數費用,恕不退還。按下確認代表您已閱讀並同意上述條款。
  1048. </Text>
  1049. <NormalButton
  1050. title={<Text className="text-white">我確認</Text>}
  1051. onPress={handleModalConfirm}
  1052. extendedStyle={styles.confirmButton}
  1053. />
  1054. </View>
  1055. </Modal>
  1056. </View>
  1057. </AccordionItem>
  1058. </View> */}
  1059. </View>
  1060. <View className="mx-[5%] flex-1">
  1061. <Text className="text-xl pb-2 mt-6" style={styles.text}>
  1062. 充電站資訊
  1063. </Text>
  1064. <View className="h-[250px]">
  1065. <ChargingStationTabView titles={['預約充電事項', '其他']} />
  1066. </View>
  1067. </View>
  1068. </ScrollView>
  1069. </SafeAreaView>
  1070. );
  1071. };
  1072. export default MakingBookingPageComponent;
  1073. const styles = StyleSheet.create({
  1074. grayColor: {
  1075. color: '#888888'
  1076. },
  1077. topLeftTriangle: {
  1078. width: 0,
  1079. height: 0,
  1080. borderLeftWidth: 50,
  1081. borderBottomWidth: 50,
  1082. borderLeftColor: '#02677D',
  1083. borderBottomColor: 'transparent',
  1084. position: 'absolute',
  1085. top: 0,
  1086. left: 0
  1087. },
  1088. modalContent: {
  1089. backgroundColor: 'white',
  1090. padding: 22,
  1091. justifyContent: 'center',
  1092. alignItems: 'center',
  1093. borderRadius: 4,
  1094. borderColor: 'rgba(0, 0, 0, 0.1)'
  1095. },
  1096. modalText: {
  1097. fontSize: 18,
  1098. marginBottom: 12,
  1099. textAlign: 'center'
  1100. },
  1101. confirmButton: {
  1102. backgroundColor: '#34667c',
  1103. paddingHorizontal: 30,
  1104. paddingVertical: 10,
  1105. borderRadius: 5
  1106. },
  1107. text: {
  1108. fontWeight: 300,
  1109. color: '#000000'
  1110. }
  1111. });