scanQrPage.tsx 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. import { CameraView, useCameraPermissions } from 'expo-camera';
  2. import { useEffect, useRef, useState } from 'react';
  3. import {
  4. ActivityIndicator,
  5. Alert,
  6. Dimensions,
  7. Pressable,
  8. ScrollView,
  9. StyleSheet,
  10. Text,
  11. Vibration,
  12. View
  13. } from 'react-native';
  14. import ChooseCarForChargingRow from '../../../../component/global/chooseCarForChargingRow';
  15. import { CrossLogoWhiteSvg, QuestionSvg } from '../../../../component/global/SVG';
  16. import { router } from 'expo-router';
  17. import { chargeStationService } from '../../../../service/chargeStationService';
  18. import { authenticationService } from '../../../../service/authService';
  19. import { walletService } from '../../../../service/walletService';
  20. import useUserInfoStore from '../../../../providers/userinfo_store';
  21. const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
  22. const ChooseCar = ({ carData, loading, selectedCar, setSelectedCar }) => {
  23. const isLargeScreen = screenHeight >= 800;
  24. const defaultImageUrl = require('../../../../assets/car1.png');
  25. return (
  26. <View
  27. style={{
  28. ...(isLargeScreen
  29. ? {
  30. marginTop: '10%',
  31. marginBottom: '12%',
  32. paddingBottom: 12
  33. }
  34. : {
  35. flex: 1,
  36. alignItems: 'center',
  37. justifyContent: 'center'
  38. })
  39. }}
  40. >
  41. <View className="justify-center items-center flex-1 ">
  42. <View
  43. style={{
  44. ...(isLargeScreen
  45. ? {}
  46. : {
  47. backgroundColor: 'rgba(0,0,0,0.7)'
  48. })
  49. }}
  50. >
  51. {loading ? (
  52. <View className="w-full">
  53. <ActivityIndicator color="#34657b" />
  54. </View>
  55. ) : (
  56. <View className="w-screen bg-[#000000B3]">
  57. <View className="flex-row items-center justify-between mx-[5%] ">
  58. <Pressable
  59. className="pt-4 "
  60. onPress={() => {
  61. if (router.canGoBack()) {
  62. router.back();
  63. } else {
  64. router.replace('mainPage');
  65. }
  66. }}
  67. >
  68. <CrossLogoWhiteSvg />
  69. </Pressable>
  70. <Text className="text-base text-white pt-2">選擇充電車輛</Text>
  71. <Text className="text-xl text-white pt-2"></Text>
  72. </View>
  73. <ScrollView
  74. horizontal={true}
  75. showsHorizontalScrollIndicator={false}
  76. contentContainerStyle={{
  77. alignItems: 'center',
  78. flexDirection: 'row',
  79. marginVertical: 12
  80. }}
  81. className="space-x-2 mx-[5%]"
  82. >
  83. {carData.map((car, index) => (
  84. <ChooseCarForChargingRow
  85. key={`${car.name}+${index}`}
  86. image={car.image}
  87. onPress={() => {
  88. setSelectedCar(car.id);
  89. console.log(car.id);
  90. }}
  91. isSelected={selectedCar === car.id}
  92. // imageUrl={image}
  93. VehicleName={car.name}
  94. isDefault={car.isDefault}
  95. />
  96. ))}
  97. </ScrollView>
  98. </View>
  99. )}
  100. </View>
  101. </View>
  102. </View>
  103. );
  104. };
  105. const ScanQrPage = () => {
  106. const { userID, setUserID } = useUserInfoStore();
  107. // State declarations
  108. const [permission, requestPermission] = useCameraPermissions();
  109. const [scanned, setScanned] = useState(false);
  110. const viewRef = useRef(null);
  111. const [scannedResult, setScannedResult] = useState('');
  112. const [selectedCar, setSelectedCar] = useState('');
  113. const now = new Date();
  114. const [loading, setLoading] = useState(true);
  115. const [loading2, setLoading2] = useState(false);
  116. const [loading3, setLoading3] = useState(false);
  117. const [carData, setCarData] = useState([]);
  118. // Effect for requesting camera permissions
  119. useEffect(() => {
  120. (async () => {
  121. const { status } = await requestPermission();
  122. if (status !== 'granted') {
  123. alert('需要相機權限以掃描QR碼');
  124. }
  125. })();
  126. }, []);
  127. // Effect for fetching user's cars
  128. useEffect(() => {
  129. const fetchAllCars = async () => {
  130. try {
  131. const response = await chargeStationService.getUserCars();
  132. if (response) {
  133. // console.log(response.data);
  134. const carTypes = response.data.map((item: any) => ({
  135. id: item.id,
  136. name: item.car_type.name,
  137. image: item.car_type.type_image_url
  138. }));
  139. console.log('carTypes', carTypes);
  140. // console.log('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', carTypes);
  141. let updatedCarTypes = [...carTypes];
  142. for (let i = 0; i < carTypes.length; i++) {
  143. const car = updatedCarTypes[i];
  144. const imageUrl = await chargeStationService.getProcessedImageUrl(car.image);
  145. updatedCarTypes[i] = {
  146. ...car,
  147. image: imageUrl
  148. };
  149. }
  150. setCarData(updatedCarTypes);
  151. // console.log('updatedCarTypes', updatedCarTypes);
  152. return true;
  153. }
  154. } catch (error) {
  155. console.log(error);
  156. } finally {
  157. setLoading(false);
  158. }
  159. };
  160. fetchAllCars();
  161. }, []);
  162. if (!permission) {
  163. return <View />;
  164. }
  165. if (!permission.granted) {
  166. return (
  167. <View className="flex-1 justify-center items-center">
  168. <Text style={{ textAlign: 'center' }}>需要相機權限以掃描QR碼,請在設定中開啟相機權限</Text>
  169. </View>
  170. );
  171. }
  172. // Function to handle barcode scanning
  173. const handleBarCodeScanned = async ({ bounds, data, type }: { bounds: any; data: any; type: any }) => {
  174. const { origin, size } = bounds;
  175. // Calculate the size of the square transparent area
  176. const transparentAreaSize = Math.min(screenWidth * 0.6, screenHeight * 0.3);
  177. const transparentAreaX = (screenWidth - transparentAreaSize) / 2;
  178. const transparentAreaY = (screenHeight - transparentAreaSize) / 2;
  179. // Check if the barcode is within the transparent area
  180. if (
  181. origin.x >= transparentAreaX &&
  182. origin.y >= transparentAreaY &&
  183. origin.x + size.width <= transparentAreaX + transparentAreaSize &&
  184. origin.y + size.height <= transparentAreaY + transparentAreaSize
  185. ) {
  186. setScanned(true);
  187. setScannedResult(data);
  188. Vibration.vibrate(100);
  189. console.log(` type: ${type} data: ${data} typeofData ${typeof data}`);
  190. try {
  191. const response = await chargeStationService.getTodayReservation();
  192. if (response) {
  193. const now = new Date();
  194. const onlyThisConnector = response.filter(
  195. (reservation: any) => reservation.connector.ConnectorID === data
  196. );
  197. onlyThisConnector.sort((a: any, b: any) => {
  198. const timeA = new Date(a.book_time).getTime();
  199. const timeB = new Date(b.book_time).getTime();
  200. return timeA - timeB;
  201. });
  202. let previousReservation = null;
  203. let upcomingReservation = null;
  204. for (let i = 0; i < onlyThisConnector.length; i++) {
  205. const reservationTime = new Date(onlyThisConnector[i].book_time).getTime();
  206. if (reservationTime <= now.getTime()) {
  207. previousReservation = onlyThisConnector[i];
  208. } else {
  209. upcomingReservation = onlyThisConnector[i];
  210. break;
  211. }
  212. }
  213. const relevantReservations = [previousReservation || null, upcomingReservation || null];
  214. console.log('Relevant reservations:', relevantReservations);
  215. const getNearestSlot = (date: Date) => {
  216. const minutes = date.getMinutes();
  217. const nearestSlot = new Date(date);
  218. nearestSlot.setMinutes(minutes < 30 ? 0 : 30);
  219. nearestSlot.setSeconds(0);
  220. nearestSlot.setMilliseconds(0);
  221. return nearestSlot;
  222. };
  223. const currentSlot = getNearestSlot(now);
  224. const nextSlot = new Date(currentSlot.getTime() + 30 * 60 * 1000);
  225. const AreOrdersAdjacentToNow = relevantReservations.map((reservation, index) => {
  226. if (!reservation) return false;
  227. const reservationTime = new Date(reservation.book_time);
  228. return index === 0
  229. ? reservationTime.getTime() === currentSlot.getTime()
  230. : reservationTime.getTime() === nextSlot.getTime();
  231. });
  232. console.log('AreOrdersAdjacentToNow:', AreOrdersAdjacentToNow);
  233. if (!AreOrdersAdjacentToNow[0] && !AreOrdersAdjacentToNow[1]) {
  234. console.log('Charging machine is available. Starting charging process...');
  235. startCharging(data);
  236. } else if (AreOrdersAdjacentToNow[0]) {
  237. const previousReservation = relevantReservations[0];
  238. if (previousReservation && previousReservation.user.id === userID) {
  239. const reservationTime = new Date(previousReservation.book_time);
  240. const timeDifference = now.getTime() - reservationTime.getTime();
  241. const minutesDifference = timeDifference / (1000 * 60);
  242. if (minutesDifference <= 15) {
  243. console.log('User arrived within 15 minutes of their reservation.');
  244. startCharging(data);
  245. } else {
  246. console.log('User arrived more than 15 minutes late for their reservation.');
  247. if (!AreOrdersAdjacentToNow[1]) {
  248. console.log('Next slot is available. Allowing charging despite late arrival.');
  249. startCharging(data);
  250. } else {
  251. Alert.alert(
  252. '預約已過期',
  253. '您的預約時間已經過期,且下一個時段已被預約。請重新預約。'
  254. );
  255. console.log('Next slot is not available. Charging not allowed.');
  256. }
  257. }
  258. } else {
  259. Alert.alert('無法使用', '此充電槍已經被預約,請選擇其他位置');
  260. }
  261. } else if (AreOrdersAdjacentToNow[1]) {
  262. const upcomingReservation = relevantReservations[1];
  263. if (upcomingReservation && upcomingReservation.user.id === userID) {
  264. const minutesUntilReservation =
  265. (new Date(upcomingReservation.book_time).getTime() - now.getTime()) / (1000 * 60);
  266. if (minutesUntilReservation <= 5) {
  267. console.log('User arrived slightly early for their upcoming reservation.');
  268. startCharging(data);
  269. } else {
  270. Alert.alert(
  271. '預約時間未到',
  272. `您的預約時間還有 ${Math.round(minutesUntilReservation)} 分鐘開始。請稍後再試。`
  273. );
  274. }
  275. } else {
  276. Alert.alert('已被預約', '此充電槍已被其他用戶預約。請選擇其他位置。');
  277. }
  278. } else {
  279. Alert.alert('無法使用', '此充電槍目前無法使用。請選擇其他位置或稍後再試。');
  280. }
  281. } else {
  282. console.log('No response from getTodayReservation');
  283. Alert.alert('系統錯誤', '無法獲取預約信息。請稍後再試。');
  284. }
  285. } catch (error) {
  286. console.error("Error fetching today's reservations:", error);
  287. Alert.alert('系統錯誤', '發生未知錯誤。請稍後再試。');
  288. }
  289. setTimeout(() => {
  290. setScanned(false);
  291. }, 2000);
  292. }
  293. };
  294. //prepare data for submission
  295. const dataForSubmission = {
  296. stationID: '2405311022116801000',
  297. connector: scannedResult,
  298. user: userID,
  299. book_time: now,
  300. end_time: now,
  301. total_power: 0,
  302. total_fee: 0,
  303. promotion_code: '',
  304. car: selectedCar,
  305. type: 'walking',
  306. is_ic_call: false
  307. };
  308. const startCharging = async (scanResult: string) => {
  309. try {
  310. if (selectedCar === '') {
  311. Alert.alert('請選擇車輛');
  312. return;
  313. }
  314. const response = await walletService.submitPayment(
  315. dataForSubmission.stationID,
  316. scanResult,
  317. dataForSubmission.user,
  318. dataForSubmission.book_time,
  319. dataForSubmission.end_time,
  320. dataForSubmission.total_power,
  321. dataForSubmission.total_fee,
  322. dataForSubmission.promotion_code,
  323. dataForSubmission.car,
  324. dataForSubmission.type,
  325. dataForSubmission.is_ic_call
  326. );
  327. if (response) {
  328. console.log('Charging started from startCharging', response);
  329. router.push('(auth)/(tabs)/(charging)/chargingPage');
  330. } else {
  331. console.log('Failed to start chargi12312312ng:', response);
  332. Alert.alert('掃描失敗 請稍後再試。', response);
  333. }
  334. } catch (error) {
  335. console.log('Failed to start charging:', error);
  336. }
  337. };
  338. return (
  339. <View style={styles.container} ref={viewRef}>
  340. {loading ? (
  341. <View className="flex-1 items-center justify-center">
  342. <ActivityIndicator />
  343. </View>
  344. ) : (
  345. <CameraView
  346. style={styles.camera}
  347. facing="back"
  348. barcodeScannerSettings={{
  349. barcodeTypes: ['qr']
  350. }}
  351. onBarcodeScanned={scanned ? undefined : handleBarCodeScanned}
  352. responsiveOrientationWhenOrientationLocked={true}
  353. >
  354. <View style={styles.overlay}>
  355. <View style={styles.topOverlay}>
  356. <ChooseCar
  357. carData={carData}
  358. loading={loading}
  359. selectedCar={selectedCar}
  360. setSelectedCar={setSelectedCar}
  361. />
  362. </View>
  363. <View style={styles.centerRow}>
  364. <View style={styles.leftOverlay}></View>
  365. <View style={styles.transparentArea}></View>
  366. <View style={styles.rightOverlay} />
  367. </View>
  368. <View className="items-center justify-between" style={styles.bottomOverlay}>
  369. <View>
  370. <Text className="text-white text-lg font-bold mt-2 text-center">
  371. 請選擇充電車輛{'\n'}及掃瞄充電座上的二維碼
  372. </Text>
  373. </View>
  374. <View className="flex-row space-x-2 items-center ">
  375. <QuestionSvg />
  376. <Pressable onPress={() => router.push('assistancePage')}>
  377. <Text className="text-white text-base">需要協助?</Text>
  378. </Pressable>
  379. </View>
  380. <View />
  381. </View>
  382. </View>
  383. </CameraView>
  384. )}
  385. </View>
  386. );
  387. };
  388. const styles = StyleSheet.create({
  389. container: {
  390. flex: 1
  391. },
  392. camera: {
  393. flex: 1
  394. },
  395. overlay: {
  396. flex: 1
  397. },
  398. topOverlay: {
  399. flex: 35,
  400. alignItems: 'center',
  401. backgroundColor: 'rgba(0,0,0,0.5)'
  402. },
  403. centerRow: {
  404. flex: 30,
  405. flexDirection: 'row'
  406. },
  407. leftOverlay: {
  408. flex: 20,
  409. backgroundColor: 'rgba(0,0,0,0.5)'
  410. },
  411. transparentArea: {
  412. flex: 60,
  413. aspectRatio: 1,
  414. position: 'relative'
  415. },
  416. rightOverlay: {
  417. flex: 20,
  418. backgroundColor: 'rgba(0,0,0,0.5)'
  419. },
  420. bottomOverlay: {
  421. flex: 35,
  422. backgroundColor: 'rgba(0,0,0,0.5)'
  423. }
  424. });
  425. export default ScanQrPage;