couponTabView.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. //the size of the TabView will follow its parent-container's size.
  2. import * as React from 'react';
  3. import {
  4. View,
  5. Text,
  6. useWindowDimensions,
  7. StyleSheet,
  8. ImageSourcePropType,
  9. ScrollView,
  10. ActivityIndicator,
  11. Pressable,
  12. Alert
  13. } from 'react-native';
  14. import { TabView, SceneMap, TabBar } from 'react-native-tab-view';
  15. import { IndividualCouponComponent } from '../accountPages/walletPageComponent';
  16. import { formatCouponDate } from '../../util/lib';
  17. import { useCallback, useEffect, useRef, useState } from 'react';
  18. import { walletService } from '../../service/walletService';
  19. import { useChargingStore } from '../../providers/scan_qr_payload_store';
  20. import { chargeStationService } from '../../service/chargeStationService';
  21. import { router } from 'expo-router';
  22. import axios from 'axios';
  23. export interface TabItem {
  24. imgURL: ImageSourcePropType;
  25. date: string;
  26. time: string;
  27. chargeStationName: string;
  28. chargeStationAddress: string;
  29. distance: string;
  30. }
  31. interface TabViewComponentProps {
  32. titles: string[];
  33. }
  34. const FirstRoute = ({
  35. coupons,
  36. loading,
  37. handleCouponClick
  38. }: {
  39. coupons: any;
  40. loading: boolean;
  41. handleCouponClick: any;
  42. }) => {
  43. return (
  44. <View className="flex-1">
  45. <ScrollView
  46. style={{ flex: 1, backgroundColor: 'white', marginTop: 14 }}
  47. contentContainerStyle={{ paddingBottom: 200 }}
  48. >
  49. <View className="flex-1 flex-col">
  50. {loading ? (
  51. <View className="items-center justify-center">
  52. <ActivityIndicator />
  53. </View>
  54. ) : (
  55. <View className="">
  56. <View>
  57. {coupons.filter(
  58. (coupon: any) =>
  59. coupon.permission == true &&
  60. coupon.is_consumed === false &&
  61. (coupon.expire_date === null || new Date(coupon.expire_date) > new Date())
  62. ).length === 0 ? (
  63. <Text className="pl-4">暫時戶口沒有優惠券。</Text>
  64. ) : (
  65. coupons
  66. .filter(
  67. (coupon: any) =>
  68. coupon.permission == true &&
  69. coupon.is_consumed === false &&
  70. (coupon.expire_date === null ||
  71. new Date(coupon.expire_date) > new Date())
  72. )
  73. .sort(
  74. (a: any, b: any) =>
  75. new Date(a.expire_date).getTime() - new Date(b.expire_date).getTime()
  76. )
  77. .slice(0, 30)
  78. .map((coupon: any, index: any) => (
  79. <IndividualCouponComponent
  80. onCouponClick={handleCouponClick}
  81. // key={coupon.redeem_code}
  82. key={`${coupon.id}-${index}`}
  83. title={coupon.coupon.name}
  84. price={coupon.coupon.amount}
  85. detail={coupon.coupon.description}
  86. date={formatCouponDate(coupon.expire_date)}
  87. setOpacity={false}
  88. redeem_code={coupon.id}
  89. />
  90. ))
  91. )}
  92. </View>
  93. </View>
  94. )}
  95. </View>
  96. </ScrollView>
  97. </View>
  98. );
  99. };
  100. const SecondRoute = ({ coupons }: { coupons: any }) => (
  101. <ScrollView style={{ flex: 1, backgroundColor: 'white', marginTop: 14 }}>
  102. <View className="flex-1 flex-col">
  103. {coupons
  104. .filter(
  105. (coupon: any) =>
  106. coupon.is_consumed === true ||
  107. (coupon.expire_date !== null && new Date(coupon.expire_date) < new Date())
  108. )
  109. .slice(0, 30)
  110. .map((coupon: any, index: any) => (
  111. <IndividualCouponComponent
  112. key={`${coupon.id}-${index}`}
  113. title={coupon.coupon.name}
  114. price={coupon.coupon.amount}
  115. detail={coupon.coupon.description}
  116. date={formatCouponDate(coupon.expire_date)}
  117. setOpacity={true}
  118. noCircle={true}
  119. />
  120. ))}
  121. </View>
  122. </ScrollView>
  123. );
  124. const CouponTabViewComponent: React.FC<TabViewComponentProps> = ({ titles }) => {
  125. const layout = useWindowDimensions();
  126. const [loading, setLoading] = useState(false);
  127. const [coupons, setCoupons] = useState([]);
  128. const [userID, setUserID] = useState('');
  129. const {
  130. current_price_store,
  131. setCurrentPriceStore,
  132. stationID,
  133. promotion_code,
  134. setPromotionCode,
  135. setCouponDetail,
  136. coupon_detail,
  137. total_power,
  138. setTotalPower,
  139. setProcessedCouponStore,
  140. setSumOfCoupon
  141. } = useChargingStore();
  142. useEffect(() => {
  143. const fetchData = async () => {
  144. try {
  145. setLoading(true);
  146. const info = await walletService.getCustomerInfo();
  147. const coupon = await walletService.getCouponForSpecificUser(info.id);
  148. setUserID(info.id);
  149. setCoupons(coupon);
  150. } catch (error) {
  151. console.log(error);
  152. } finally {
  153. setLoading(false);
  154. }
  155. };
  156. fetchData();
  157. }, []);
  158. //fetch current price for coupon valid calculation
  159. useEffect(() => {
  160. const fetchCurrentPrice = async () => {
  161. try {
  162. const response = await chargeStationService.getOriginalPriceInPay(stationID);
  163. setCurrentPriceStore(response);
  164. } catch (error) {
  165. // More specific error handling
  166. if (axios.isAxiosError(error)) {
  167. const errorMessage = error.response?.data?.message || 'Network error occurred';
  168. Alert.alert('Error', `Unable to fetch price: ${errorMessage}`, [
  169. {
  170. text: 'OK',
  171. onPress: () => {
  172. cleanupData();
  173. router.push('/mainPage');
  174. }
  175. }
  176. ]);
  177. } else {
  178. Alert.alert('Error', 'An unexpected error occurred while fetching the price', [
  179. {
  180. text: 'OK',
  181. onPress: () => {
  182. cleanupData();
  183. router.push('/mainPage');
  184. }
  185. }
  186. ]);
  187. }
  188. }
  189. };
  190. fetchCurrentPrice();
  191. }, []);
  192. const handleCouponClick = async (clickedCoupon: string) => {
  193. let temp_promotion_code = [...promotion_code];
  194. if (!current_price_store) {
  195. Alert.alert('Error', 'Unable to fetch price', [
  196. {
  197. text: 'OK',
  198. onPress: () => {
  199. cleanupData();
  200. router.push('/mainPage');
  201. }
  202. }
  203. ]);
  204. return;
  205. }
  206. let orderAmount = current_price_store * total_power;
  207. //when i click on a coupon, if coupone doesnt already exist in the stack, i add it to the stack
  208. if (!promotion_code.includes(clickedCoupon)) {
  209. const found_coupon = coupons.find((coupon: any) => coupon.id === clickedCoupon);
  210. temp_promotion_code = [...promotion_code, found_coupon.id];
  211. try {
  212. const valid = await chargeStationService.validateCoupon(temp_promotion_code, orderAmount);
  213. if (valid === true) {
  214. setCouponDetail([...coupon_detail, found_coupon]);
  215. setPromotionCode([...promotion_code, clickedCoupon]);
  216. } else {
  217. Alert.alert('不符合使用優惠券的條件', '請查看優惠卷的詳情,例如是否需要滿足最低消費金額。');
  218. }
  219. } catch (error) {
  220. console.log(error);
  221. }
  222. } else {
  223. //coupon already exists, this de-select the coupon
  224. const index_of_clicked_coupon = promotion_code.findIndex((i) => i === clickedCoupon);
  225. const newPromotionCode = [...promotion_code];
  226. newPromotionCode.splice(index_of_clicked_coupon, 1);
  227. setPromotionCode(newPromotionCode);
  228. const newCouponDetail = coupon_detail.filter((detail: any) => detail.id !== clickedCoupon);
  229. setCouponDetail(newCouponDetail);
  230. }
  231. };
  232. const cleanupData = () => {
  233. setPromotionCode([]);
  234. setCouponDetail([]);
  235. setProcessedCouponStore([]);
  236. setSumOfCoupon(0);
  237. setTotalPower(null);
  238. };
  239. const renderScene = useCallback(
  240. ({ route }: { route: any }) => {
  241. switch (route.key) {
  242. case 'firstRoute':
  243. return <FirstRoute coupons={coupons} loading={loading} handleCouponClick={handleCouponClick} />;
  244. case 'secondRoute':
  245. return <SecondRoute coupons={coupons} />;
  246. default:
  247. return null;
  248. }
  249. },
  250. [coupons, loading, handleCouponClick]
  251. );
  252. const [routes] = React.useState([
  253. { key: 'firstRoute', title: titles[0] },
  254. { key: 'secondRoute', title: titles[1] }
  255. ]);
  256. const [index, setIndex] = React.useState(0);
  257. const renderTabBar = (props: any) => (
  258. <TabBar
  259. {...props}
  260. indicatorStyle={{
  261. backgroundColor: '#025c72'
  262. }}
  263. style={{
  264. backgroundColor: 'white',
  265. borderColor: '#DBE4E8',
  266. elevation: 0,
  267. marginHorizontal: 15,
  268. borderBottomWidth: 0.5
  269. }}
  270. />
  271. );
  272. return (
  273. <TabView
  274. navigationState={{ index, routes }}
  275. renderScene={renderScene}
  276. onIndexChange={setIndex}
  277. initialLayout={{ width: layout.width }}
  278. renderTabBar={renderTabBar}
  279. commonOptions={{
  280. label: ({ route, focused }) => (
  281. <Text
  282. style={{
  283. color: focused ? '#025c72' : '#888888',
  284. fontWeight: focused ? '900' : 'thin',
  285. fontSize: 20
  286. }}
  287. >
  288. {route.title}
  289. </Text>
  290. )
  291. }}
  292. />
  293. );
  294. };
  295. export default CouponTabViewComponent;
  296. const styles = StyleSheet.create({
  297. container: { flexDirection: 'row' },
  298. image: { width: 100, height: 100, margin: 15, borderRadius: 10 },
  299. textContainer: { flexDirection: 'column', gap: 8, marginTop: 20 },
  300. floatingButton: {
  301. elevation: 5,
  302. shadowColor: '#000',
  303. shadowOffset: { width: 0, height: 2 },
  304. shadowOpacity: 0.25,
  305. shadowRadius: 3.84
  306. }
  307. });