homePage.tsx 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. import {
  2. View,
  3. Text,
  4. ScrollView,
  5. FlatList,
  6. Pressable,
  7. ActivityIndicator,
  8. Image,
  9. Modal,
  10. Alert,
  11. TextInput
  12. } from 'react-native';
  13. import NormalButton from '../global/normal_button';
  14. import { SafeAreaView } from 'react-native-safe-area-context';
  15. import { router, useFocusEffect } from 'expo-router';
  16. import { useColorScheme } from 'nativewind';
  17. import RecentlyBookedScrollView from '../global/recentlyBookedScrollView';
  18. import {
  19. BellIconSvg,
  20. HomeIconSvg,
  21. MyBookingIconSvg,
  22. WhatsAppSvg,
  23. WalletSvg,
  24. MyWalletSvg,
  25. QrCodeIconSvg,
  26. VipCodeIconSvg
  27. } from '../global/SVG';
  28. import { AuthContext } from '../../context/AuthProvider';
  29. import { useCallback, useContext, useEffect, useState } from 'react';
  30. import { authenticationService } from '../../service/authService';
  31. import { chargeStationService } from '../../service/chargeStationService';
  32. import { walletService } from '../../service/walletService';
  33. import useUserInfoStore from '../../providers/userinfo_store';
  34. import NormalInput from '../global/normal_input';
  35. import { notificationStorage } from '../notificationStorage';
  36. import { handleGoWhatsApp } from '../../util/index';
  37. import { useChargingStore } from '../../providers/scan_qr_payload_store';
  38. interface HomePageProps {}
  39. const HomePage: React.FC<HomePageProps> = () => {
  40. const {
  41. promotion_code,
  42. stationID,
  43. sum_of_coupon,
  44. scanned_qr_code,
  45. coupon_detail,
  46. total_power,
  47. processed_coupon_store,
  48. setPromotionCode,
  49. setCouponDetail,
  50. setTotalPower,
  51. setProcessedCouponStore,
  52. setSumOfCoupon,
  53. setCurrentPriceStore
  54. } = useChargingStore();
  55. const now = new Date();
  56. const { user } = useContext(AuthContext);
  57. const { userID, currentPrice, setUserID, setCurrentPrice, setNotifySessionID } = useUserInfoStore();
  58. const { colorScheme, toggleColorScheme } = useColorScheme();
  59. const [showLicencePlateMessage, setShowLicencePlateMessage] = useState<boolean>(false);
  60. const [licensePlate, setLicensePlate] = useState<string>('');
  61. const [showConfirmationModal, setShowConfirmationModal] = useState<boolean>(false);
  62. const [showOnboarding, setShowOnboarding] = useState(true);
  63. const [mainPromotion, setMainPromotion] = useState([]);
  64. const [mainPromotionImage, setMainPromotionImage] = useState('');
  65. const [reservationAfter2025, setReservationAfter2025] = useState([]);
  66. const [isLoadingReservations, setIsLoadingReservations] = useState(true);
  67. const [unreadCount, setUnreadCount] = useState(0);
  68. useEffect(() => {
  69. const fetchIDandCheckLicensePlate = async () => {
  70. try {
  71. const response = await authenticationService.getUserInfo();
  72. //if success, set user ID,
  73. if (response) {
  74. setNotifySessionID(response.data.notify_session_id);
  75. setUserID(response.data.id);
  76. //after setting id, also check if the user has a valid license plate, if not, show message
  77. if (!response.data.cars || !Array.isArray(response.data.cars)) {
  78. Alert.alert('無法檢測車輛資訊', '請稍後再試');
  79. setShowLicencePlateMessage(false);
  80. }
  81. if (response.data.cars.length === 1 && response.data.cars[0].license_plate === '0000') {
  82. setShowLicencePlateMessage(true);
  83. }
  84. } else {
  85. Alert.alert('fail to set user/notification session ID');
  86. }
  87. } catch (error) {
  88. console.log(error);
  89. }
  90. };
  91. const fetchCurrentPrice = async () => {
  92. try {
  93. const response = await chargeStationService.getCurrentPrice();
  94. if (response) {
  95. setCurrentPrice(response);
  96. }
  97. } catch (error) {
  98. console.log('main page fetch current price error', error);
  99. }
  100. };
  101. const fetchMainPromotion = async () => {
  102. try {
  103. const response = await chargeStationService.getAdvertise();
  104. if (response) {
  105. const mainPromo = response.filter((item: any) => item.is_main)[0];
  106. setMainPromotion(mainPromo);
  107. if (mainPromo) {
  108. const mainPromoImage = await chargeStationService.getProcessedImageUrl(mainPromo.image_url);
  109. if (mainPromoImage) {
  110. setMainPromotionImage(mainPromoImage);
  111. }
  112. }
  113. }
  114. } catch (error) {
  115. console.log('Error fetching promotion:', error);
  116. }
  117. };
  118. fetchMainPromotion();
  119. const fetchWithAllSettled = async () => {
  120. const results = await Promise.allSettled([
  121. fetchIDandCheckLicensePlate(),
  122. fetchCurrentPrice(),
  123. fetchMainPromotion()
  124. ]);
  125. };
  126. fetchWithAllSettled();
  127. }, []);
  128. const cleanupData = () => {
  129. setPromotionCode([]);
  130. setCouponDetail([]);
  131. setProcessedCouponStore([]);
  132. setSumOfCoupon(0);
  133. setTotalPower(null);
  134. };
  135. useFocusEffect(
  136. useCallback(() => {
  137. let isActive = true;
  138. const fetchData = async () => {
  139. setIsLoadingReservations(true); // Start loading
  140. try {
  141. const results = await Promise.allSettled([
  142. chargeStationService.fetchReservationHistories(),
  143. chargeStationService.getAdvertise()
  144. ]);
  145. if (!isActive) return;
  146. // Handle reservation data
  147. if (results[0].status === 'fulfilled') {
  148. const year2025 = new Date('2025-02-01T00:00:00.000Z');
  149. const reservationAfter2025 = results[0].value.filter((r: any) => {
  150. const date = new Date(r.createdAt);
  151. return date > year2025;
  152. });
  153. setReservationAfter2025(reservationAfter2025);
  154. } else if (results[0].status === 'rejected') {
  155. Alert.alert('Error fetching reservations:', results[0].reason);
  156. }
  157. // Get viewed notifications
  158. const viewedNotifications = await notificationStorage.getViewedNotifications();
  159. let totalUnread = 0;
  160. // Count unread reservations
  161. if (results[0].status === 'fulfilled') {
  162. const unreadReservations = reservationAfter2025.filter((r: any) => {
  163. return !viewedNotifications.some((vn: any) => vn.id === r.id);
  164. });
  165. totalUnread += unreadReservations.length;
  166. }
  167. // Count unread promotions
  168. if (results[1].status === 'fulfilled') {
  169. const unreadPromotions = results[1].value.filter((p: any) => {
  170. return !viewedNotifications.some((vn) => vn.id === p.id);
  171. });
  172. totalUnread += unreadPromotions.length;
  173. }
  174. setUnreadCount(totalUnread);
  175. if (results[0].status === 'fulfilled') {
  176. const reservationHistories = results[0].value;
  177. if (reservationHistories || Array.isArray(reservationHistories)) {
  178. const unpaidPenalties = reservationHistories.filter(
  179. (reservation: any) => reservation.penalty_fee > 0 && reservation.penalty_paid_status === false
  180. );
  181. const mostRecentUnpaidReservation = unpaidPenalties.reduce((mostRecent: any, current: any) => {
  182. return new Date(mostRecent.created_at) > new Date(current.created_at) ? mostRecent : current;
  183. }, unpaidPenalties[0]);
  184. console.log('mostRecentUnpaidReservation', mostRecentUnpaidReservation);
  185. if (unpaidPenalties.length > 0) {
  186. Alert.alert(
  187. '未付罰款',
  188. '您有未支付的罰款。請先支付罰款後再重新掃描充電。',
  189. [
  190. {
  191. text: '查看詳情',
  192. onPress: () => {
  193. // Navigate to a page showing penalty details
  194. cleanupData();
  195. router.push({
  196. pathname: '(auth)/(tabs)/(home)/penaltyPaymentPage',
  197. params: {
  198. book_time: mostRecentUnpaidReservation.book_time,
  199. end_time: mostRecentUnpaidReservation.end_time,
  200. actual_end_time: mostRecentUnpaidReservation.actual_end_time,
  201. penalty_fee: mostRecentUnpaidReservation.penalty_fee,
  202. format_order_id: mostRecentUnpaidReservation.format_order_id,
  203. id: mostRecentUnpaidReservation.id
  204. }
  205. });
  206. }
  207. },
  208. {
  209. text: '關閉',
  210. onPress: () => {
  211. cleanupData();
  212. }
  213. }
  214. ],
  215. { cancelable: false }
  216. );
  217. return;
  218. }
  219. }
  220. }
  221. } catch (error) {
  222. if (!isActive) return;
  223. console.log('Error fetching data');
  224. } finally {
  225. if (isActive) {
  226. setIsLoadingReservations(false);
  227. }
  228. }
  229. };
  230. fetchData();
  231. return () => {
  232. isActive = false;
  233. };
  234. }, [])
  235. );
  236. const saveLicensePlate = async (licensePlate: string) => {
  237. try {
  238. const response = await chargeStationService.addCar(
  239. licensePlate,
  240. '1834d087-bfc1-4f90-8f09-805e3d9422b5',
  241. 'f599470d-53a5-4026-99c0-2dab34c77f39',
  242. true
  243. );
  244. if (response === true) {
  245. console.log('License plate saved successfully');
  246. } else {
  247. Alert.alert('無法保存車牌號碼', '請稍後再試');
  248. }
  249. } catch (error) {
  250. Alert.alert('暫時無法保存車牌號碼', '請稍後再試');
  251. }
  252. };
  253. return (
  254. <SafeAreaView edges={['top', 'left', 'right']} className="flex-1 bg-white">
  255. {/* Add Modal component */}
  256. {mainPromotionImage && (
  257. <Modal
  258. animationType="fade"
  259. transparent={true}
  260. visible={showOnboarding}
  261. onRequestClose={() => { setShowOnboarding(false)}}
  262. >
  263. <Pressable
  264. className="flex-1 bg-black/50 items-center justify-center"
  265. onPress={() => setShowOnboarding(false)}
  266. >
  267. <View className="w-[98%] rounded-2xl ">
  268. <Image
  269. source={{ uri: mainPromotionImage }}
  270. className="w-full aspect-square "
  271. resizeMode="contain"
  272. />
  273. <Text className="text-center mt-4 mb-2 text-gray-200">點擊任意位置關閉</Text>
  274. </View>
  275. </Pressable>
  276. </Modal>
  277. )}
  278. {showLicencePlateMessage && (
  279. <Modal
  280. animationType="fade"
  281. transparent={true}
  282. visible={showLicencePlateMessage}
  283. onRequestClose={() => setShowLicencePlateMessage(false)}
  284. >
  285. <View className="flex-1 bg-black/50 items-center justify-center">
  286. {!showConfirmationModal ? (
  287. // License Plate Input Modal
  288. <View className="flex flex-col rounded-2xl bg-white overflow-hidden w-[80%]">
  289. <View className="bg-[#E3F2F8]">
  290. <Text className="text-base lg:text-lg font-[500] text-center p-4">
  291. 請添加您的車牌號碼
  292. </Text>
  293. </View>
  294. <View className="p-4 ">
  295. <Text className="text-sm lg:text-base font-[500] text-left mb-4">
  296. 為更好地為您提供服務,請在您的帳戶中添加車牌號碼。
  297. </Text>
  298. <NormalInput
  299. value={licensePlate}
  300. placeholder="車牌號碼"
  301. onChangeText={(s) => setLicensePlate(s)}
  302. extendedStyle={{ borderRadius: 12, marginBottom: 0 }}
  303. textContentType="none"
  304. autoComplete="off"
  305. keyboardType="default"
  306. />
  307. </View>
  308. <View className="pr-4 pl-4 pb-4 ">
  309. <NormalButton
  310. title={<Text className="text-white text-sm lg:text-lg">確定</Text>}
  311. onPress={() => {
  312. //here when users click confirm, i want to pop another modal that say you have entered "xxxxxx", click confirm to continue
  313. if (!licensePlate.trim()) {
  314. Alert.alert('請輸入車牌號碼');
  315. return;
  316. }
  317. if (licensePlate.trim().length < 4 || licensePlate.trim().length > 10) {
  318. Alert.alert('無效的車牌號碼', '請輸入有效的車牌號碼');
  319. return;
  320. }
  321. setShowConfirmationModal(true);
  322. }}
  323. />
  324. </View>
  325. </View>
  326. ) : (
  327. // Confirmation Modal
  328. <View className="flex flex-col rounded-2xl bg-white overflow-hidden w-[80%]">
  329. <View className="bg-[#E3F2F8]">
  330. <Text className="text-base lg:text-lg font-[500] text-center p-4">
  331. 確認車牌號碼
  332. </Text>
  333. </View>
  334. <View className="p-4">
  335. <Text className="text-sm lg:text-base font-[500] text-center mb-4">
  336. 您輸入的車牌號碼為:{licensePlate}
  337. </Text>
  338. </View>
  339. <View className="flex-row p-4 space-x-4">
  340. <View className="flex-1">
  341. <NormalButton
  342. title={<Text className="text-white text-sm lg:text-lg">取消</Text>}
  343. onPress={() => setShowConfirmationModal(false)}
  344. />
  345. </View>
  346. <View className="flex-1">
  347. <NormalButton
  348. title={<Text className="text-white text-sm lg:text-lg">確認</Text>}
  349. onPress={() => {
  350. saveLicensePlate(licensePlate);
  351. setShowConfirmationModal(false);
  352. setShowLicencePlateMessage(false);
  353. setLicensePlate('');
  354. }}
  355. />
  356. </View>
  357. </View>
  358. </View>
  359. )}
  360. </View>
  361. </Modal>
  362. )}
  363. <ScrollView showsVerticalScrollIndicator={false} className="flex-1 mx-[5%] ">
  364. <View className=" flex-1 pt-8 ">
  365. <View className="flex-row items-center pb-4">
  366. <HomeIconSvg />
  367. <View className="pl-2 flex-1 flex-column ">
  368. <View className="flex-row justify-between mr-[10%]">
  369. <Text className="text-lg text-left pb-1">您好!</Text>
  370. <View className="relative z-5">
  371. <Pressable
  372. onPress={() => router.push({ pathname: 'notificationPage' })}
  373. disabled={isLoadingReservations}
  374. className="z-10 w-10 items-center justify-center"
  375. hitSlop={{ top: 20, bottom: 20, left: 20, right: 20 }}
  376. >
  377. <View className="w-6 h-6">
  378. <BellIconSvg />
  379. </View>
  380. {unreadCount > 0 && (
  381. <View className="absolute -top-2 -right-[0.5] bg-red-500 rounded-full w-5 h-5 items-center justify-center">
  382. <Text className="text-white text-xs font-bold">{unreadCount}</Text>
  383. </View>
  384. )}
  385. </Pressable>
  386. <Pressable className="z-10 top-9 right-0" onPress={() => handleGoWhatsApp()}>
  387. <View className="w-8 h-8">
  388. <WhatsAppSvg />
  389. </View>
  390. </Pressable>
  391. </View>
  392. </View>
  393. <Text className="text-4xl font-light ">{user?.nickname}</Text>
  394. </View>
  395. </View>
  396. <View className=" flex-1 justify-center ">
  397. {/* <Pressable onPress={() => router.push('searchPage')}>
  398. <View
  399. style={{
  400. borderWidth: 1,
  401. padding: 24,
  402. borderRadius: 12,
  403. borderColor: '#bbbbbb',
  404. maxWidth: '100%'
  405. }}
  406. >
  407. <Text style={{ color: '#888888', fontSize: 16 }}>搜尋充電站或地區..</Text>
  408. </View>
  409. </Pressable> */}
  410. </View>
  411. </View>
  412. <View className="flex-1">
  413. <View className="my-4">
  414. <NormalButton
  415. onPress={() => router.push('scanQrPage')}
  416. // onPress={() => router.push('optionPage')}
  417. title={
  418. <View className="flex flex-row justify-start">
  419. <QrCodeIconSvg />
  420. <Text className="text-white font-bold text-lg ml-2">掃描及充電</Text>
  421. </View>
  422. }
  423. extendedStyle={{
  424. alignItems: 'flex-start',
  425. padding: 24
  426. }}
  427. />
  428. </View>
  429. <View className="flex-1 flex-row justify-between gap-6">
  430. {/* <View className="flex-1">
  431. <NormalButton
  432. // onPress={() => router.push('bookingMenuPage')}
  433. onPress={() => Alert.alert('即將推出', '此功能即將推出,敬請期待!')}
  434. //onPress={() => notificationStorage.clearStorage()}
  435. title={
  436. <View className="flex flex-row space-x-2 items-center ">
  437. <MyBookingIconSvg />
  438. <Text className="text-white font-bold text-lg ml-2">我的預約</Text>
  439. </View>
  440. }
  441. extendedStyle={{
  442. alignItems: 'flex-start',
  443. padding: 24
  444. }}
  445. />
  446. </View> */}
  447. <View className="flex-1">
  448. <NormalButton
  449. onPress={() => router.push('/(account)/(wallet)/walletPage')}
  450. title={
  451. <View className="flex flex-row space-x-2 items-center">
  452. <MyWalletSvg />
  453. <Text className="text-white font-bold text-lg ml-2">錢包</Text>
  454. </View>
  455. }
  456. extendedStyle={{
  457. alignItems: 'flex-start',
  458. padding: 24
  459. }}
  460. />
  461. </View>
  462. </View>
  463. <View className="mt-4">
  464. <NormalButton
  465. // onPress={() => console.log('掃瞄及充電')}
  466. onPress={() => router.push('vipQrPage')}
  467. title={
  468. <View className="flex flex-row items-center space-x-2">
  469. <VipCodeIconSvg />
  470. <Text className="text-white font-bold text-lg ml-2">專屬會員二維碼</Text>
  471. </View>
  472. }
  473. extendedStyle={{
  474. alignItems: 'flex-start',
  475. padding: 24
  476. }}
  477. />
  478. </View>
  479. </View>
  480. </ScrollView>
  481. </SafeAreaView>
  482. );
  483. };
  484. export default HomePage;