resultDetailPageComponent.tsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  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. indicatorStyle={{
  60. backgroundColor: '#000000',
  61. height: 1
  62. }}
  63. style={{
  64. backgroundColor: 'white',
  65. elevation: 0,
  66. marginHorizontal: 15,
  67. borderBottomWidth: 0.5
  68. }}
  69. />
  70. );
  71. return (
  72. <TabView
  73. navigationState={{ index, routes }}
  74. renderScene={renderScene}
  75. onIndexChange={setIndex}
  76. initialLayout={{ width: layout.width }}
  77. renderTabBar={renderTabBar}
  78. commonOptions={{
  79. label: ({ route, focused }) => (
  80. <Text
  81. style={{
  82. color: focused ? '#000000' : '#CCCCCC',
  83. fontWeight: focused ? '300' : 'thin',
  84. fontSize: 17
  85. }}
  86. >
  87. {route.title}
  88. </Text>
  89. )
  90. }}
  91. />
  92. );
  93. };
  94. const ResultDetailPageComponent = () => {
  95. const params = useLocalSearchParams();
  96. const chargeStationID = params.chargeStationID as string;
  97. const chargeStationName = params.chargeStationName as string;
  98. const chargeStationAddress = params.chargeStationAddress as string;
  99. const availableConnectorsFromParams = params.availableConnectors;
  100. const imageSource = params.imageSource;
  101. const stationLng = params.stationLng as string;
  102. const stationLat = params.stationLat as string;
  103. const [isLoading, setIsLoading] = useState(true);
  104. // const chargeStationLat = params.chargeStationLat as string;
  105. // const chargeStationLng = params.chargeStationLng as string;
  106. const [currentLocation, setCurrentLocation] = useState<Location.LocationObject | null>(null);
  107. const [distance, setDistance] = useState<string | null>(null);
  108. const [coordinates, setCoordinates] = useState<StationCoordinates | null>(null);
  109. const [price, setPrice] = useState('');
  110. const [availableConnectors, setAvailableConnectors] = useState<number | null>(
  111. availableConnectorsFromParams ? Number(availableConnectorsFromParams) : null
  112. );
  113. const [newAvailableConnectors, setNewAvailableConnectors] = useState([]);
  114. useEffect(() => {
  115. const getCurrentLocation = async () => {
  116. let { status } = await Location.requestForegroundPermissionsAsync();
  117. if (status !== 'granted') {
  118. console.error('Permission to access location was denied');
  119. return;
  120. }
  121. let location = await Location.getLastKnownPositionAsync({});
  122. setCurrentLocation(location);
  123. };
  124. getCurrentLocation();
  125. }, []);
  126. useEffect(() => {
  127. const fetchPrice = async () => {
  128. try {
  129. const price = await chargeStationService.fetchChargeStationPrice(chargeStationID);
  130. setPrice(price);
  131. } catch (error) {
  132. console.error('Error fetching price:', error);
  133. }
  134. };
  135. fetchPrice();
  136. }, []);
  137. useFocusEffect(
  138. useCallback(() => {
  139. setIsLoading(true);
  140. let isMounted = true; // Simple cleanup flag
  141. const fetchAllConnectors = async () => {
  142. try {
  143. const newAvailableConnectors = await chargeStationService.NewfetchAvailableConnectors();
  144. // Only update state if component is still mounted
  145. if (isMounted) {
  146. setNewAvailableConnectors(newAvailableConnectors);
  147. }
  148. } catch (error) {
  149. console.error('Fetch error:', error);
  150. }
  151. };
  152. fetchAllConnectors();
  153. setIsLoading(false);
  154. // Simple cleanup - prevents state updates if component unmounts
  155. return () => {
  156. isMounted = false;
  157. };
  158. }, []) // Add any missing dependencies here if needed
  159. );
  160. const handleNavigationPress = (StationLat: string, StationLng: string) => {
  161. console.log('starting navigation press in resultDetail', StationLat, StationLng);
  162. if (StationLat && StationLng) {
  163. const label = encodeURIComponent(chargeStationName);
  164. const googleMapsUrl = `https://www.google.com/maps/search/?api=1&query=${StationLat},${StationLng}`;
  165. // Fallback URL for web browser
  166. const webUrl = `https://www.google.com/maps/dir/?api=1&destination=${StationLat},${StationLng}`;
  167. Linking.canOpenURL(googleMapsUrl)
  168. .then((supported) => {
  169. if (supported) {
  170. Linking.openURL(googleMapsUrl);
  171. } else {
  172. Linking.openURL(webUrl).catch((err) => {
  173. console.error('An error occurred', err);
  174. Alert.alert(
  175. 'Error',
  176. "Unable to open Google Maps. Please make sure it's installed on your device.",
  177. [{ text: 'OK' }],
  178. { cancelable: false }
  179. );
  180. });
  181. }
  182. })
  183. .catch((err) => console.error('An error occurred', err));
  184. }
  185. };
  186. const handleNavigaationPress = () => {
  187. const latitude = chargeStationLat;
  188. const longitude = chargeStationLng;
  189. console.log('latitude', latitude);
  190. console.log('longitude', longitude);
  191. const label = encodeURIComponent(chargeStationName);
  192. // Google Maps App URL
  193. const googleMapsUrl = `https://www.google.com/maps/search/?api=1&query=${latitude},${longitude}`;
  194. // Fallback URL for web browser
  195. const webUrl = `https://www.google.com/maps/dir/?api=1&destination=${latitude},${longitude}`;
  196. Linking.canOpenURL(googleMapsUrl)
  197. .then((supported) => {
  198. if (supported) {
  199. Linking.openURL(googleMapsUrl);
  200. } else {
  201. Linking.openURL(webUrl).catch((err) => {
  202. console.error('An error occurred', err);
  203. Alert.alert(
  204. 'Error',
  205. "Unable to open Google Maps. Please make sure it's installed on your device.",
  206. [{ text: 'OK' }],
  207. { cancelable: false }
  208. );
  209. });
  210. }
  211. })
  212. .catch((err) => console.error('An error occurred', err));
  213. };
  214. const handleAddBooking = () => {
  215. if (coordinates) {
  216. router.push({
  217. pathname: 'makingBookingPage',
  218. params: {
  219. chargeStationID: chargeStationID,
  220. chargeStationAddress: chargeStationAddress,
  221. chargeStationName: chargeStationName,
  222. chargeStationLat: coordinates.StationLat.toString(),
  223. chargeStationLng: coordinates.StationLng.toString()
  224. }
  225. });
  226. }
  227. };
  228. return (
  229. <SafeAreaView edges={['top', 'left', 'right']} className="flex-1 bg-white">
  230. <ScrollView className="flex-1 ">
  231. <View className="relative">
  232. <Image
  233. source={{ uri: imageSource }}
  234. resizeMode="cover"
  235. style={{ flex: 1, width: '100%', height: 300 }}
  236. />
  237. <View className="absolute top-8 left-7 ">
  238. <Pressable
  239. onPress={() => {
  240. if (router.canGoBack()) {
  241. router.back();
  242. } else {
  243. router.replace('./');
  244. }
  245. }}
  246. >
  247. <PreviousPageSvg />
  248. </Pressable>
  249. </View>
  250. </View>
  251. <View className="flex-column mx-[5%] mt-[5%]">
  252. <View>
  253. <Text className="text-3xl ">{chargeStationName}</Text>
  254. </View>
  255. <View className="flex-row justify-between items-center">
  256. <Text className="text-base" style={{ color: '#888888' }}>
  257. {chargeStationAddress}
  258. </Text>
  259. <NormalButton
  260. title={
  261. <View className="flex-row items-center justify-center text-center space-x-1">
  262. <DirectionLogoSvg />
  263. <Text className="text-base ">路線</Text>
  264. </View>
  265. }
  266. onPress={() => handleNavigationPress(stationLat, stationLng)}
  267. extendedStyle={{
  268. backgroundColor: '#E3F2F8',
  269. borderRadius: 61,
  270. paddingHorizontal: 20,
  271. paddingVertical: 6
  272. }}
  273. />
  274. </View>
  275. <View className="flex-row space-x-2 items-center pb-4 ">
  276. <CheckMarkLogoSvg />
  277. <Text>Walk-In</Text>
  278. {/* <Text>{distance} </Text> */}
  279. </View>
  280. {/* {coordinates ? (
  281. <NormalButton
  282. title={
  283. <View className="pr-2">
  284. <Text
  285. style={{
  286. color: '#FFFFFF',
  287. fontWeight: 700,
  288. fontSize: 20
  289. }}
  290. >
  291. + 新增預約
  292. </Text>
  293. </View>
  294. }
  295. // onPress={() => console.log('ab')}
  296. onPress={handleAddBooking}
  297. />
  298. ) : (
  299. <NormalButton
  300. title={
  301. <View className="pr-2">
  302. <Text
  303. style={{
  304. color: '#FFFFFF',
  305. fontWeight: 700,
  306. fontSize: 20
  307. }}
  308. >
  309. <ActivityIndicator />
  310. </Text>
  311. </View>
  312. }
  313. // onPress={() => console.log('ab')}
  314. onPress={handleAddBooking}
  315. />
  316. )} */}
  317. <View
  318. className="flex-1 flex-row min-h-[20px] border-slate-300 my-6 rounded-2xl"
  319. style={{ borderWidth: 1 }}
  320. >
  321. <View className="flex-1 m-4">
  322. <View className="flex-1 flex-row ">
  323. <View className=" flex-1 flex-column ustify-between">
  324. <Text className="text-xl " style={styles.text}>
  325. 收費
  326. </Text>
  327. <View className="flex-row items-center space-x-2">
  328. <Text className="text-3xl text-[#02677D]">${price}</Text>
  329. <Text style={styles.text}>每度電</Text>
  330. </View>
  331. </View>
  332. <View className="items-center justify-center">
  333. <View className="w-[1px] h-[60%] bg-[#CCCCCC]" />
  334. </View>
  335. <View className=" flex-1 pl-4 flex-column justify-between">
  336. <Text className="text-xl " style={styles.text}>
  337. 現時可用充電槍數目
  338. </Text>
  339. <View className="flex-row items-center space-x-2">
  340. {isLoading ? (
  341. <Text>Loading...</Text>
  342. ) : (
  343. // <ActivityIndicator />
  344. <Text className="text-3xl text-[#02677D]">
  345. {
  346. newAvailableConnectors.find(
  347. (station: any) => station.stationID === chargeStationID
  348. )?.availableConnectors
  349. }
  350. </Text>
  351. )}
  352. </View>
  353. </View>
  354. </View>
  355. </View>
  356. </View>
  357. </View>
  358. <View className="min-h-[300px]">
  359. <Text className="text-xl pb-2 mx-[5%]" style={styles.text}>
  360. 充電站資訊
  361. </Text>
  362. <ChargingStationTabView titles={['預約充電事項', '其他']} />
  363. </View>
  364. </ScrollView>
  365. </SafeAreaView>
  366. );
  367. };
  368. export default ResultDetailPageComponent;
  369. const styles = StyleSheet.create({
  370. text: {
  371. fontWeight: 300,
  372. color: '#000000'
  373. }
  374. });