homePage.tsx 25 KB

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