resultDetailPageComponent.tsx 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  1. import {
  2. View,
  3. Text,
  4. ScrollView,
  5. Image,
  6. useWindowDimensions,
  7. StyleSheet,
  8. Pressable,
  9. Platform,
  10. Linking,
  11. ActivityIndicator,
  12. Alert
  13. } from 'react-native';
  14. import React, { useCallback, useEffect, useMemo, useState } from 'react';
  15. import { SceneMap, TabBar, TabView } from 'react-native-tab-view';
  16. import NormalButton from '../global/normal_button';
  17. import { router, useFocusEffect, useLocalSearchParams } from 'expo-router';
  18. import { CheckMarkLogoSvg, DirectionLogoSvg, PreviousPageSvg } from '../global/SVG';
  19. import { SafeAreaView } from 'react-native-safe-area-context';
  20. import { chargeStationService } from '../../service/chargeStationService';
  21. import * as Location from 'expo-location';
  22. import { calculateDistance } from '../global/distanceCalculator';
  23. interface ChargingStationTabViewProps {
  24. titles: string[];
  25. }
  26. interface StationCoordinates {
  27. StationLat: number;
  28. StationLng: number;
  29. }
  30. const ChargingStationTabView: React.FC<ChargingStationTabViewProps> = ({ titles }) => {
  31. const layout = useWindowDimensions();
  32. //tab 1
  33. const FirstRoute = () => (
  34. <ScrollView style={{ flex: 1, marginHorizontal: '5%' }}>
  35. <Text className="text-lg" style={styles.text}>
  36. 由於充電站車流眾多, 敬請客戶務必於預約時間的十五分鐘內到達充電站。
  37. 若客戶逾時超過15分鐘,系統將視作自動放棄預約,客戶需要重新預約一次。 本公司有權保留全數費用,恕不退還。
  38. </Text>
  39. </ScrollView>
  40. );
  41. //tab 2
  42. const SecondRoute = () => (
  43. <ScrollView style={{ flex: 1, marginHorizontal: '5%' }}>
  44. <Text className="text-lg " style={styles.text}></Text>
  45. </ScrollView>
  46. );
  47. const renderScene = SceneMap({
  48. firstRoute: FirstRoute,
  49. secondRoute: SecondRoute
  50. });
  51. const [routes] = React.useState([
  52. { key: 'firstRoute', title: titles[0] },
  53. { key: 'secondRoute', title: titles[1] }
  54. ]);
  55. const [index, setIndex] = React.useState(0);
  56. const renderTabBar = (props: any) => (
  57. <TabBar
  58. {...props}
  59. renderLabel={({ route, focused }) => (
  60. <Text
  61. style={{
  62. color: focused ? '#000000' : '#CCCCCC',
  63. fontWeight: focused ? '300' : 'thin',
  64. fontSize: 17
  65. }}
  66. >
  67. {route.title}
  68. </Text>
  69. )}
  70. indicatorStyle={{
  71. backgroundColor: '#000000',
  72. height: 1
  73. }}
  74. style={{
  75. backgroundColor: 'white',
  76. elevation: 0,
  77. marginHorizontal: 15,
  78. borderBottomWidth: 0.5
  79. }}
  80. />
  81. );
  82. return (
  83. <TabView
  84. navigationState={{ index, routes }}
  85. renderScene={renderScene}
  86. onIndexChange={setIndex}
  87. initialLayout={{ width: layout.width }}
  88. renderTabBar={renderTabBar}
  89. />
  90. );
  91. };
  92. const ResultDetailPageComponent = () => {
  93. const params = useLocalSearchParams();
  94. const chargeStationID = params.chargeStationID as string;
  95. const chargeStationName = params.chargeStationName as string;
  96. const chargeStationAddress = params.chargeStationAddress as string;
  97. const availableConnectorsFromParams = params.availableConnectors;
  98. const imageSource = params.imageSource;
  99. const stationLng = params.stationLng as string;
  100. const stationLat = params.stationLat as string;
  101. const [isLoading, setIsLoading] = useState(true);
  102. // const chargeStationLat = params.chargeStationLat as string;
  103. // const chargeStationLng = params.chargeStationLng as string;
  104. const [currentLocation, setCurrentLocation] = useState<Location.LocationObject | null>(null);
  105. const [distance, setDistance] = useState<string | null>(null);
  106. const [coordinates, setCoordinates] = useState<StationCoordinates | null>(null);
  107. const [price, setPrice] = useState('');
  108. const [availableConnectors, setAvailableConnectors] = useState<number | null>(
  109. availableConnectorsFromParams ? Number(availableConnectorsFromParams) : null
  110. );
  111. const [newAvailableConnectors, setNewAvailableConnectors] = useState([]);
  112. useEffect(() => {
  113. const getCurrentLocation = async () => {
  114. let { status } = await Location.requestForegroundPermissionsAsync();
  115. if (status !== 'granted') {
  116. console.error('Permission to access location was denied');
  117. return;
  118. }
  119. let location = await Location.getLastKnownPositionAsync({});
  120. setCurrentLocation(location);
  121. };
  122. getCurrentLocation();
  123. }, []);
  124. // useEffect(() => {
  125. // const getDistance = async () => {
  126. // if (currentLocation) {
  127. // let stationLat, stationLng;
  128. // if (params.chargeStationLat && params.chargeStationLng) {
  129. // stationLat = Number(params.chargeStationLat);
  130. // stationLng = Number(params.chargeStationLng);
  131. // } else if (coordinates) {
  132. // stationLat = coordinates.StationLat;
  133. // stationLng = coordinates.StationLng;
  134. // } else {
  135. // console.log('No station coordinates available');
  136. // return;
  137. // }
  138. // try {
  139. // const distance = await calculateDistance(stationLat, stationLng, currentLocation);
  140. // setDistance(formatDistance(distance));
  141. // } catch (error) {
  142. // console.error('Error calculating distance:', error);
  143. // }
  144. // }
  145. // };
  146. // getDistance();
  147. // }, [params.chargeStationLat, params.chargeStationLng, coordinates, currentLocation]);
  148. useEffect(() => {
  149. const fetchPrice = async () => {
  150. try {
  151. const price = await chargeStationService.fetchChargeStationPrice(chargeStationID);
  152. setPrice(price);
  153. } catch (error) {
  154. console.error('Error fetching price:', error);
  155. }
  156. };
  157. fetchPrice();
  158. }, []);
  159. // console.log(chargeStationLat, chargeStationLng);
  160. // const memoizedCoordinates = useMemo(() => {
  161. // if (params.chargeStationLat && params.chargeStationLng) {
  162. // return {
  163. // StationLat: parseFloat(params.chargeStationLat as string),
  164. // StationLng: parseFloat(params.chargeStationLng as string)
  165. // };
  166. // }
  167. // return null;
  168. // }, [params.chargeStationLat, params.chargeStationLng]);
  169. // useEffect(() => {
  170. // const fetchCoordinates = async () => {
  171. // // If coordinates are provided in params, use them
  172. // if (memoizedCoordinates) {
  173. // setCoordinates(memoizedCoordinates);
  174. // } else {
  175. // // If not provided, fetch from API
  176. // try {
  177. // const response = await chargeStationService.fetchChargeStations();
  178. // if (response && response.length > 0) {
  179. // const station = response.find((s) => s.StationID === chargeStationID);
  180. // if (station) {
  181. // setCoordinates({
  182. // StationLat: station.StationLat,
  183. // StationLng: station.StationLng
  184. // });
  185. // }
  186. // }
  187. // } catch (error) {
  188. // console.error('Error fetching coordinates:', error);
  189. // }
  190. // }
  191. // };
  192. // fetchCoordinates();
  193. // }, [chargeStationID, memoizedCoordinates]);
  194. // useEffect(() => {
  195. // const fetchAvailableConnectors = async () => {
  196. // // Skip fetching if we already have the value from params
  197. // if (availableConnectorsFromParams) return;
  198. // try {
  199. // const connectors = await chargeStationService.fetchAvailableConnectors(chargeStationID);
  200. // console.log('connectors number from resultDetailPage', connectors);
  201. // setAvailableConnectors(connectors);
  202. // } catch (error) {
  203. // console.error('Error fetching available connectors:', error);
  204. // setAvailableConnectors(null);
  205. // }
  206. // };
  207. // fetchAvailableConnectors();
  208. // }, [chargeStationID, availableConnectorsFromParams]);
  209. useFocusEffect(
  210. useCallback(() => {
  211. setIsLoading(true);
  212. let isMounted = true; // Simple cleanup flag
  213. const fetchAllConnectors = async () => {
  214. try {
  215. const newAvailableConnectors = await chargeStationService.NewfetchAvailableConnectors();
  216. // Only update state if component is still mounted
  217. if (isMounted) {
  218. setNewAvailableConnectors(newAvailableConnectors);
  219. }
  220. } catch (error) {
  221. console.error('Fetch error:', error);
  222. }
  223. };
  224. fetchAllConnectors();
  225. setIsLoading(false);
  226. // Simple cleanup - prevents state updates if component unmounts
  227. return () => {
  228. isMounted = false;
  229. };
  230. }, []) // Add any missing dependencies here if needed
  231. );
  232. // const formatDistance = (distanceInMeters: number): string => {
  233. // if (distanceInMeters < 1000) {
  234. // return `${Math.round(distanceInMeters)}米`;
  235. // } else {
  236. // const distanceInKm = distanceInMeters / 1000;
  237. // return `${distanceInKm.toFixed(1)}公里`;
  238. // }
  239. // };
  240. const handleNavigationPress = (StationLat: string, StationLng: string) => {
  241. console.log('starting navigation press in resultDetail', StationLat, StationLng);
  242. if (StationLat && StationLng) {
  243. const label = encodeURIComponent(chargeStationName);
  244. const googleMapsUrl = `https://www.google.com/maps/search/?api=1&query=${StationLat},${StationLng}`;
  245. // Fallback URL for web browser
  246. const webUrl = `https://www.google.com/maps/dir/?api=1&destination=${StationLat},${StationLng}`;
  247. Linking.canOpenURL(googleMapsUrl)
  248. .then((supported) => {
  249. if (supported) {
  250. Linking.openURL(googleMapsUrl);
  251. } else {
  252. Linking.openURL(webUrl).catch((err) => {
  253. console.error('An error occurred', err);
  254. Alert.alert(
  255. 'Error',
  256. "Unable to open Google Maps. Please make sure it's installed on your device.",
  257. [{ text: 'OK' }],
  258. { cancelable: false }
  259. );
  260. });
  261. }
  262. })
  263. .catch((err) => console.error('An error occurred', err));
  264. }
  265. };
  266. const handleNavigaationPress = () => {
  267. const latitude = chargeStationLat;
  268. const longitude = chargeStationLng;
  269. console.log('latitude', latitude);
  270. console.log('longitude', longitude);
  271. const label = encodeURIComponent(chargeStationName);
  272. // Google Maps App URL
  273. const googleMapsUrl = `https://www.google.com/maps/search/?api=1&query=${latitude},${longitude}`;
  274. // Fallback URL for web browser
  275. const webUrl = `https://www.google.com/maps/dir/?api=1&destination=${latitude},${longitude}`;
  276. Linking.canOpenURL(googleMapsUrl)
  277. .then((supported) => {
  278. if (supported) {
  279. Linking.openURL(googleMapsUrl);
  280. } else {
  281. Linking.openURL(webUrl).catch((err) => {
  282. console.error('An error occurred', err);
  283. Alert.alert(
  284. 'Error',
  285. "Unable to open Google Maps. Please make sure it's installed on your device.",
  286. [{ text: 'OK' }],
  287. { cancelable: false }
  288. );
  289. });
  290. }
  291. })
  292. .catch((err) => console.error('An error occurred', err));
  293. };
  294. const handleAddBooking = () => {
  295. if (coordinates) {
  296. router.push({
  297. pathname: 'makingBookingPage',
  298. params: {
  299. chargeStationID: chargeStationID,
  300. chargeStationAddress: chargeStationAddress,
  301. chargeStationName: chargeStationName,
  302. chargeStationLat: coordinates.StationLat.toString(),
  303. chargeStationLng: coordinates.StationLng.toString()
  304. }
  305. });
  306. }
  307. };
  308. return (
  309. <SafeAreaView edges={['top', 'left', 'right']} className="flex-1 bg-white">
  310. <ScrollView className="flex-1 ">
  311. <View className="relative">
  312. <Image
  313. source={{ uri: imageSource }}
  314. resizeMode="cover"
  315. style={{ flex: 1, width: '100%', height: 300 }}
  316. />
  317. <View className="absolute top-8 left-7 ">
  318. <Pressable
  319. onPress={() => {
  320. if (router.canGoBack()) {
  321. router.back();
  322. } else {
  323. router.replace('./');
  324. }
  325. }}
  326. >
  327. <PreviousPageSvg />
  328. </Pressable>
  329. </View>
  330. </View>
  331. <View className="flex-column mx-[5%] mt-[5%]">
  332. <View>
  333. <Text className="text-3xl ">{chargeStationName}</Text>
  334. </View>
  335. <View className="flex-row justify-between items-center">
  336. <Text className="text-base" style={{ color: '#888888' }}>
  337. {chargeStationAddress}
  338. </Text>
  339. <NormalButton
  340. title={
  341. <View className="flex-row items-center justify-center text-center space-x-1">
  342. <DirectionLogoSvg />
  343. <Text className="text-base ">路線</Text>
  344. </View>
  345. }
  346. onPress={() => handleNavigationPress(stationLat, stationLng)}
  347. extendedStyle={{
  348. backgroundColor: '#E3F2F8',
  349. borderRadius: 61,
  350. paddingHorizontal: 20,
  351. paddingVertical: 6
  352. }}
  353. />
  354. </View>
  355. <View className="flex-row space-x-2 items-center pb-4 ">
  356. <CheckMarkLogoSvg />
  357. <Text>Walk-In</Text>
  358. {/* <Text>{distance} </Text> */}
  359. </View>
  360. {/* {coordinates ? (
  361. <NormalButton
  362. title={
  363. <View className="pr-2">
  364. <Text
  365. style={{
  366. color: '#FFFFFF',
  367. fontWeight: 700,
  368. fontSize: 20
  369. }}
  370. >
  371. + 新增預約
  372. </Text>
  373. </View>
  374. }
  375. // onPress={() => console.log('ab')}
  376. onPress={handleAddBooking}
  377. />
  378. ) : (
  379. <NormalButton
  380. title={
  381. <View className="pr-2">
  382. <Text
  383. style={{
  384. color: '#FFFFFF',
  385. fontWeight: 700,
  386. fontSize: 20
  387. }}
  388. >
  389. <ActivityIndicator />
  390. </Text>
  391. </View>
  392. }
  393. // onPress={() => console.log('ab')}
  394. onPress={handleAddBooking}
  395. />
  396. )} */}
  397. <View
  398. className="flex-1 flex-row min-h-[20px] border-slate-300 my-6 rounded-2xl"
  399. style={{ borderWidth: 1 }}
  400. >
  401. <View className="flex-1 m-4">
  402. <View className="flex-1 flex-row ">
  403. <View className=" flex-1 flex-column ustify-between">
  404. <Text className="text-xl " style={styles.text}>
  405. 收費
  406. </Text>
  407. <View className="flex-row items-center space-x-2">
  408. <Text className="text-3xl text-[#02677D]">${price}</Text>
  409. <Text style={styles.text}>每度電</Text>
  410. </View>
  411. </View>
  412. <View className="items-center justify-center">
  413. <View className="w-[1px] h-[60%] bg-[#CCCCCC]" />
  414. </View>
  415. <View className=" flex-1 pl-4 flex-column justify-between">
  416. <Text className="text-xl " style={styles.text}>
  417. 現時可用充電槍數目
  418. </Text>
  419. <View className="flex-row items-center space-x-2">
  420. {isLoading ? (
  421. <Text>Loading...</Text>
  422. ) : (
  423. // <ActivityIndicator />
  424. <Text className="text-3xl text-[#02677D]">
  425. {
  426. newAvailableConnectors.find(
  427. (station: any) => station.stationID === chargeStationID
  428. )?.availableConnectors
  429. }
  430. </Text>
  431. )}
  432. </View>
  433. </View>
  434. </View>
  435. </View>
  436. </View>
  437. </View>
  438. <View className="min-h-[300px]">
  439. <Text className="text-xl pb-2 mx-[5%]" style={styles.text}>
  440. 充電站資訊
  441. </Text>
  442. <ChargingStationTabView titles={['預約充電事項', '其他']} />
  443. </View>
  444. </ScrollView>
  445. </SafeAreaView>
  446. );
  447. };
  448. export default ResultDetailPageComponent;
  449. const styles = StyleSheet.create({
  450. text: {
  451. fontWeight: 300,
  452. color: '#000000'
  453. }
  454. });