scanQrPage.tsx 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  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. const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
  21. const ChooseCar = ({ carData, loading, selectedCar, setSelectedCar }) => {
  22. const isLargeScreen = screenHeight >= 800;
  23. const defaultImageUrl = require('../../../../assets/car1.png');
  24. return (
  25. <View
  26. style={{
  27. ...(isLargeScreen
  28. ? {
  29. marginTop: '10%',
  30. marginBottom: '12%',
  31. paddingBottom: 12
  32. }
  33. : {
  34. flex: 1,
  35. alignItems: 'center',
  36. justifyContent: 'center'
  37. })
  38. }}
  39. >
  40. <View className="justify-center items-center flex-1 ">
  41. <View
  42. style={{
  43. ...(isLargeScreen
  44. ? {}
  45. : {
  46. backgroundColor: 'rgba(0,0,0,0.7)'
  47. })
  48. }}
  49. >
  50. {loading ? (
  51. <View className="w-full">
  52. <ActivityIndicator color="#34657b" />
  53. </View>
  54. ) : (
  55. <View className="w-full bg-[#000000B3]">
  56. <View className="flex-row items-center justify-between mx-[5%] ">
  57. <Pressable
  58. className="pt-4 "
  59. onPress={() => {
  60. if (router.canGoBack()) {
  61. router.back();
  62. } else {
  63. router.replace('mainPage');
  64. }
  65. }}
  66. >
  67. <CrossLogoWhiteSvg />
  68. </Pressable>
  69. <Text className="text-base text-white pt-2">選擇充電車輛</Text>
  70. <Text className="text-xl text-white pt-2"></Text>
  71. </View>
  72. <ScrollView
  73. horizontal={true}
  74. showsHorizontalScrollIndicator={false}
  75. contentContainerStyle={{
  76. alignItems: 'center',
  77. flexDirection: 'row',
  78. marginVertical: 12
  79. }}
  80. className="space-x-2 mx-[5%]"
  81. >
  82. {carData.map((car, index) => (
  83. <ChooseCarForChargingRow
  84. key={`${car.name}+${index}`}
  85. image={car.image}
  86. onPress={() => {
  87. setSelectedCar(car.id);
  88. console.log(car.id);
  89. }}
  90. isSelected={selectedCar === car.id}
  91. // imageUrl={image}
  92. VehicleName={car.name}
  93. isDefault={car.isDefault}
  94. />
  95. ))}
  96. </ScrollView>
  97. </View>
  98. )}
  99. </View>
  100. </View>
  101. </View>
  102. );
  103. };
  104. const ScanQrPage = () => {
  105. // State declarations
  106. const [permission, requestPermission] = useCameraPermissions();
  107. const [scanned, setScanned] = useState(false);
  108. const viewRef = useRef(null);
  109. const [scannedResult, setScannedResult] = useState('');
  110. const [selectedCar, setSelectedCar] = useState('');
  111. const [userID, setUserID] = useState<string>('');
  112. const now = new Date();
  113. const [loading, setLoading] = useState(false);
  114. const [loading2, setLoading2] = useState(false);
  115. const [carData, setCarData] = useState([]);
  116. // Effect for requesting camera permissions
  117. useEffect(() => {
  118. (async () => {
  119. const { status } = await requestPermission();
  120. if (status !== 'granted') {
  121. alert('需要相機權限以掃描QR碼');
  122. }
  123. })();
  124. }, []);
  125. // Effect for fetching user's cars
  126. useEffect(() => {
  127. const fetchAllCars = async () => {
  128. setLoading(true);
  129. try {
  130. const response = await chargeStationService.getUserCars();
  131. if (response) {
  132. // console.log(response.data);
  133. const carTypes = response.data.map((item: any) => ({
  134. id: item.id,
  135. name: item.car_type.name,
  136. image: item.car_type.type_image_url
  137. }));
  138. // console.log('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', carTypes);
  139. let updatedCarTypes = [...carTypes];
  140. for (let i = 0; i < carTypes.length; i++) {
  141. const car = updatedCarTypes[i];
  142. const imageUrl = await chargeStationService.getProcessedImageUrl(car.image);
  143. updatedCarTypes[i] = {
  144. ...car,
  145. image: imageUrl
  146. };
  147. }
  148. setCarData(updatedCarTypes);
  149. return true;
  150. }
  151. } catch (error) {
  152. console.log(error);
  153. } finally {
  154. setLoading(false);
  155. }
  156. };
  157. fetchAllCars();
  158. }, []);
  159. // Effect for fetching user ID
  160. useEffect(() => {
  161. const fetchID = async () => {
  162. try {
  163. const response = await authenticationService.getUserInfo();
  164. if (response) {
  165. setUserID(response.data.id);
  166. } else {
  167. console.log('fail to set user ID');
  168. }
  169. } catch (error) {
  170. console.log(error);
  171. }
  172. };
  173. fetchID();
  174. }, []);
  175. if (!permission) {
  176. return <View />;
  177. }
  178. if (!permission.granted) {
  179. return (
  180. <View className="flex-1 justify-center items-center">
  181. <Text style={{ textAlign: 'center' }}>需要相機權限以掃描QR碼,請在設定中開啟相機權限</Text>
  182. </View>
  183. );
  184. }
  185. // // Function to handle barcode scanning
  186. // const handleBarCodeScanned = ({ bounds, data, type }: { bounds: any; data: any; type: any }) => {
  187. // const { origin, size } = bounds;
  188. // // Calculate the size of the square transparent area
  189. // const transparentAreaSize = Math.min(screenWidth * 0.6, screenHeight * 0.3);
  190. // const transparentAreaX = (screenWidth - transparentAreaSize) / 2;
  191. // const transparentAreaY = (screenHeight - transparentAreaSize) / 2;
  192. // // Check if the barcode is within the transparent area
  193. // if (
  194. // origin.x >= transparentAreaX &&
  195. // origin.y >= transparentAreaY &&
  196. // origin.x + size.width <= transparentAreaX + transparentAreaSize &&
  197. // origin.y + size.height <= transparentAreaY + transparentAreaSize
  198. // ) {
  199. // setScanned(true);
  200. // setScannedResult(data);
  201. // Vibration.vibrate(100);
  202. // console.log(` type: ${type} data: ${data} typeofData ${typeof data}`);
  203. // startCharging(data);
  204. // setTimeout(() => {
  205. // setScanned(false);
  206. // }, 2000);
  207. // }
  208. // };
  209. // Function to handle barcode scanning
  210. const handleBarCodeScanned = ({ bounds, data, type }: { bounds: any; data: any; type: any }) => {
  211. const { origin, size } = bounds;
  212. // Calculate the size of the square transparent area
  213. const transparentAreaSize = Math.min(screenWidth * 0.6, screenHeight * 0.3);
  214. const transparentAreaX = (screenWidth - transparentAreaSize) / 2;
  215. const transparentAreaY = (screenHeight - transparentAreaSize) / 2;
  216. // Check if the barcode is within the transparent area
  217. if (
  218. origin.x >= transparentAreaX &&
  219. origin.y >= transparentAreaY &&
  220. origin.x + size.width <= transparentAreaX + transparentAreaSize &&
  221. origin.y + size.height <= transparentAreaY + transparentAreaSize
  222. ) {
  223. setScanned(true);
  224. setScannedResult(data);
  225. Vibration.vibrate(100);
  226. console.log(` type: ${type} data: ${data} typeofData ${typeof data}`);
  227. //HERE I will not be startCharging, I will call a function to check
  228. startCharging(data);
  229. setTimeout(() => {
  230. setScanned(false);
  231. }, 2000);
  232. }
  233. };
  234. //WAIT FOR KUN TO CREATE ANOTHER API,
  235. //USE THE NEW API FOR SCAN QR CODE FUNCTIONALITY
  236. // ***********************************************************
  237. // **************************************************************
  238. // **************************************************************
  239. // **************************************************************
  240. // **************************************************************
  241. // **************************************************************
  242. // **************************************************************
  243. // **************************************************************
  244. const checkCurrentReservation = async () => {
  245. const now = new Date();
  246. try {
  247. console.log('i am checking current reservation');
  248. const response = await chargeStationService.fetchReservationHistories();
  249. if (response) {
  250. //check if any reservation is within 15 minutes
  251. console.log('response of checkCurrentReservation', response);
  252. const filteredResponse = response.filter((r) => {
  253. const bookTime = new Date(r.book_time);
  254. const fifteenMinutesAfterBookTime = new Date(bookTime.getTime() + 15 * 60 * 1000);
  255. const isWithin15MinutesAndStatus6 =
  256. now > bookTime && now <= fifteenMinutesAfterBookTime && r.status.id === '6';
  257. return isWithin15MinutesAndStatus6;
  258. });
  259. if (filteredResponse.length > 0) {
  260. console.log('there is a reservation within 15 minutes');
  261. return filteredResponse;
  262. }
  263. }
  264. //meaning no reservation within 15 minutes
  265. else {
  266. console.log('no reservation within 15 minutes');
  267. return false;
  268. }
  269. } catch (error) {
  270. console.log(error);
  271. }
  272. };
  273. const checkQrCode = async (data: string) => {
  274. setLoading2(true);
  275. const connectorID = data;
  276. try {
  277. const response = await checkCurrentReservation();
  278. if (response) {
  279. console.log('checking if scan code is for this reservation, from checkQrCode', response);
  280. //if there is a reservation within 15 minutes
  281. const checkIfScanCodeIsForThisReservation = response.connector.id === data;
  282. if (checkIfScanCodeIsForThisReservation) {
  283. startCharging(data);
  284. return true;
  285. } else {
  286. console.log(
  287. 'The user indeed has a valid reservation, but the reservation is not for this charging machine.'
  288. );
  289. Alert.alert('您預約了另一座充電座\n請前往正確的充電座進行充電。');
  290. return false;
  291. }
  292. } else {
  293. //if there is no reservation within 15 minutes
  294. //meaing these are walk in clients wanting to charge
  295. //first, i have to check the current time,
  296. //if the availability of current time slot is not available then i return alert
  297. }
  298. } catch (error) {
  299. console.log(error);
  300. }
  301. };
  302. // **************************************************************
  303. // **************************************************************
  304. // **************************************************************
  305. // **************************************************************
  306. // **************************************************************
  307. // **************************************************************
  308. // **************************************************************
  309. //prepare data for submission
  310. const dataForSubmission = {
  311. stationID: '2405311022116801000',
  312. connector: scannedResult,
  313. user: userID,
  314. book_time: now,
  315. end_time: now,
  316. total_power: 0,
  317. total_fee: 0,
  318. promotion_code: '',
  319. car: selectedCar,
  320. type: 'walking'
  321. };
  322. const startCharging = async (scanResult: string) => {
  323. try {
  324. if (selectedCar === '') {
  325. Alert.alert('請選擇車輛');
  326. return;
  327. }
  328. const response = await walletService.submitPayment(
  329. dataForSubmission.stationID,
  330. scanResult,
  331. dataForSubmission.user,
  332. dataForSubmission.book_time,
  333. dataForSubmission.end_time,
  334. dataForSubmission.total_power,
  335. dataForSubmission.total_fee,
  336. dataForSubmission.promotion_code,
  337. dataForSubmission.car,
  338. dataForSubmission.type
  339. );
  340. if (response) {
  341. console.log('Charging started from startCharging', response);
  342. router.push('(auth)/(tabs)/(charging)/chargingPage');
  343. } else {
  344. console.log('Failed to start chargi12312312ng:', response);
  345. Alert.alert('掃描失敗 請稍後再試。', response);
  346. }
  347. } catch (error) {
  348. console.log('Failed to start charging:', error);
  349. }
  350. };
  351. const abc = new Date();
  352. async function checkIfConnectorIsAvailable(stationID: string) {
  353. try {
  354. const apiResponse = await chargeStationService.fetchSpecificChargeStation(stationID);
  355. if (apiResponse) {
  356. console.log('apiResponse', apiResponse);
  357. const inputDate = new Date();
  358. const formattedDate =
  359. (inputDate.getMonth() + 1).toString().padStart(2, '0') +
  360. '/' +
  361. inputDate.getDate().toString().padStart(2, '0');
  362. // Get the hours and minutes
  363. const hours = inputDate.getHours();
  364. const minutes = inputDate.getMinutes();
  365. // Round down to the nearest 30-minute slot
  366. const slotStart = `${hours.toString().padStart(2, '0')}:${minutes < 30 ? '00' : '30'}`;
  367. console.log('slotStart', slotStart);
  368. const findSlot = (date, start) => {
  369. const dayData = apiResponse.find((day) => day.date === date);
  370. if (!dayData) return null;
  371. return dayData.range.find((slot) => slot.start === start);
  372. };
  373. const currentSlot = findSlot(formattedDate, slotStart);
  374. console.log('currentSlot', currentSlot);
  375. let nextSlotStart;
  376. if (slotStart.endsWith('30')) {
  377. nextSlotStart = `${(hours + 1).toString().padStart(2, '0')}:00`;
  378. } else {
  379. nextSlotStart = `${hours.toString().padStart(2, '0')}:30`;
  380. }
  381. const nextSlot = findSlot(formattedDate, nextSlotStart);
  382. console.log('nextSlot', nextSlot);
  383. return {
  384. currentSlot,
  385. nextSlot
  386. };
  387. } else {
  388. console.log('no response from fetchSpecificChargeStation in scanQRcode Page');
  389. return false;
  390. }
  391. } catch (error) {
  392. console.log(error);
  393. }
  394. }
  395. const adbc = checkIfConnectorIsAvailable('2405311022116801000');
  396. console.log(adbc);
  397. return (
  398. <View style={styles.container} ref={viewRef}>
  399. {loading ? (
  400. <View className="flex-1 items-center justify-center">
  401. <ActivityIndicator />
  402. </View>
  403. ) : (
  404. <CameraView
  405. style={styles.camera}
  406. facing="back"
  407. barcodeScannerSettings={{
  408. barcodeTypes: ['qr']
  409. }}
  410. onBarcodeScanned={scanned ? undefined : handleBarCodeScanned}
  411. responsiveOrientationWhenOrientationLocked={true}
  412. >
  413. <View style={styles.overlay}>
  414. <View style={styles.topOverlay}>
  415. <ChooseCar
  416. carData={carData}
  417. loading={loading}
  418. selectedCar={selectedCar}
  419. setSelectedCar={setSelectedCar}
  420. />
  421. </View>
  422. <View style={styles.centerRow}>
  423. <View style={styles.leftOverlay}></View>
  424. <View style={styles.transparentArea}></View>
  425. <View style={styles.rightOverlay} />
  426. </View>
  427. <View className="items-center justify-between" style={styles.bottomOverlay}>
  428. <View>
  429. <Text className="text-white text-lg font-bold mt-2 text-center">
  430. 請選擇充電車輛{'\n'}及掃瞄充電座上的二維碼
  431. </Text>
  432. </View>
  433. <View className="flex-row space-x-2 items-center ">
  434. <QuestionSvg />
  435. <Pressable onPress={() => router.push('assistancePage')}>
  436. <Text className="text-white text-base">需要協助?</Text>
  437. </Pressable>
  438. </View>
  439. <View />
  440. </View>
  441. </View>
  442. </CameraView>
  443. )}
  444. </View>
  445. );
  446. };
  447. const styles = StyleSheet.create({
  448. container: {
  449. flex: 1
  450. },
  451. camera: {
  452. flex: 1
  453. },
  454. overlay: {
  455. flex: 1
  456. },
  457. topOverlay: {
  458. flex: 35,
  459. alignItems: 'center',
  460. backgroundColor: 'rgba(0,0,0,0.5)'
  461. },
  462. centerRow: {
  463. flex: 30,
  464. flexDirection: 'row'
  465. },
  466. leftOverlay: {
  467. flex: 20,
  468. backgroundColor: 'rgba(0,0,0,0.5)'
  469. },
  470. transparentArea: {
  471. flex: 60,
  472. aspectRatio: 1,
  473. position: 'relative'
  474. },
  475. rightOverlay: {
  476. flex: 20,
  477. backgroundColor: 'rgba(0,0,0,0.5)'
  478. },
  479. bottomOverlay: {
  480. flex: 35,
  481. backgroundColor: 'rgba(0,0,0,0.5)'
  482. }
  483. });
  484. export default ScanQrPage;