From 9d5869c3c2ca5c15b466de8e4d0b0bb2cf40cc11 Mon Sep 17 00:00:00 2001 From: vinay kumar Date: Fri, 8 Aug 2025 19:02:15 +0530 Subject: [PATCH] Work on Payments screen --- android/app/src/main/AndroidManifest.xml | 2 +- app/(tabs)/_layout.tsx | 2 + app/(tabs)/index.tsx | 24 +- app/(tabs)/payments.tsx | 763 ++++++++++++++++++++- app/_layout.tsx | 1 - assets/icons/refresh.svg | 8 + components/Payments/PaymentHistoryCard.tsx | 152 ++++ components/home/MetricCard.tsx | 2 +- services/axiosClient.ts | 1 + services/i18n/locals/en.json | 1 + services/socket.ts | 15 +- store/paymentSlice.ts | 152 ++++ store/rootReducer.ts | 2 + store/telemetrySlice.ts | 7 + store/userSlice.ts | 1 + utils/Payments.ts | 12 + 16 files changed, 1126 insertions(+), 19 deletions(-) create mode 100644 assets/icons/refresh.svg create mode 100644 components/Payments/PaymentHistoryCard.tsx create mode 100644 store/paymentSlice.ts create mode 100644 utils/Payments.ts diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index c49d875..98f1759 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -18,7 +18,7 @@ + android:value="AIzaSyCJ4IkKM0ybTOwvdylLVx9BxL2ZV1PEvRk"/> diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index e413c29..b01043c 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -8,6 +8,7 @@ import { useSnackbar } from "@/contexts/Snackbar"; import NetInfo from "@react-native-community/netinfo"; import { getUserDetails } from "@/store/userSlice"; import { useDispatch } from "react-redux"; +import { logout } from "@/store/authSlice"; export default function TabLayout() { const { isLoggedIn } = useSelector((state: RootState) => state.auth); @@ -38,6 +39,7 @@ export default function TabLayout() { initSocket(); } catch (error) { console.error("Failed to fetch user details", error); + logout(); showSnackbar("Failed to fetch user details", "error"); } }; diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index bde4015..394c1ad 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -25,6 +25,9 @@ import { useRouter } from "expo-router"; import ProfileImage from "@/components/home/Profile"; import { calculateBearing, calculateDistance } from "@/utils/Map"; import { useDispatch } from "react-redux"; +import RefreshIcon from "@/assets/icons/refresh.svg"; +import { disconnectSocket, initSocket } from "@/services/socket"; +import { setTelemetryError, setTelemetryLoading } from "@/store/telemetrySlice"; export default function HomeScreen() { const { t } = useTranslation(); @@ -53,7 +56,6 @@ export default function HomeScreen() { const warrantyStartDate = data?.batteries[0]?.warranty_start_date || null; const warrantyEndDate = data?.batteries[0]?.warranty_end_date || null; - const step = 0.001; useEffect(() => { if (lat && lon) { if (prevPosition) { @@ -108,6 +110,9 @@ export default function HomeScreen() { ), headerRight: () => ( + handleManualRefresh()}> + + { @@ -115,7 +120,7 @@ export default function HomeScreen() { setIsSupportModalVisible(true); }} > - + { @@ -141,6 +146,18 @@ export default function HomeScreen() { ); }; + const handleManualRefresh = async () => { + dispatch(setTelemetryLoading()); + + try { + disconnectSocket(); + await initSocket(); + } catch (err) { + console.error("Refresh failed", err); + dispatch(setTelemetryError("Refresh failed")); + } + }; + return ( @@ -187,6 +204,7 @@ export default function HomeScreen() { longitudeDelta: 0.0421, }} // customMapStyle={mapStyle} + userInterfaceStyle="light" > { + try { + // Parse the formatted date from API (e.g., "7 Aug 2025, 5:23:58 pm") + return dateString.split(",")[0]; // Extract just the date part + } catch { + return dateString; + } +}; + +// Format time for payment history +const formatPaymentTime = (dateString: string) => { + try { + // Extract time and day info from the formatted date + const parts = dateString.split(","); + if (parts.length > 1) { + const timePart = parts[1].trim(); // "5:23:58 pm" + const date = new Date(dateString); + const dayName = date.toLocaleDateString("en-US", { weekday: "long" }); + return `${timePart} | ${dayName}`; + } + return dateString; + } catch { + return dateString; + } +}; export default function PaymentsTabScreen() { + const navigation = useNavigation(); + const { data } = useSelector((state: RootState) => state.user); + const router = useRouter(); + const { showSnackbar } = useSnackbar(); + + const [emiDetails, setEmiDetails] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const [isSupportModalVisible, setIsSupportModalVisible] = useState(false); + const [isEndReached, setIsEndReached] = useState(false); + + //payment history states + const [paymentHistory, setPaymentHistory] = useState( + [] + ); + const [isHistoryLoading, setIsHistoryLoading] = useState(false); + const [showFullHistory, setShowFullHistory] = useState(false); + const [currentPage, setCurrentPage] = useState(1); + const [hasMorePages, setHasMorePages] = useState(true); + const [isLoadingMore, setIsLoadingMore] = useState(false); + + const vehicle = + Array.isArray(data?.vehicles) && data.vehicles.length > 0 + ? data.vehicles[0] + : null; + const battery = + Array.isArray(data?.batteries) && data.batteries.length > 0 + ? data.batteries[0] + : null; + + const model = vehicle?.model ?? "---"; + const chasisNumber = vehicle?.chasis_number ?? "---"; + + // Fetch EMI details + useEffect(() => { + const fetchEmiDetails = async () => { + try { + setIsLoading(true); + + const response = await api.get(`/api/v1/emi-details`); + const result: EmiResponse = response.data; + + if (result.success && result.data.length > 0) { + setEmiDetails(result.data[0]); + } else { + showSnackbar("No EMI details found", "error"); + } + } catch (err) { + console.error("Error fetching EMI details:", err); + const errorMessage = + err instanceof Error ? err.message : "Something went wrong"; + showSnackbar(errorMessage, "error"); + } finally { + setIsLoading(false); + } + }; + + fetchEmiDetails(); + }, []); + + useLayoutEffect(() => { + navigation.setOptions({ + headerStyle: { + backgroundColor: "#F3F5F8", + }, + headerTitle: () => ( + + {model} + {chasisNumber} + + ), + headerRight: () => ( + + { + console.log("Support Pressed"); + setIsSupportModalVisible(true); + }} + > + + + { + router.push("/user/profile"); + }} + > + router.push("/user/profile")} + textSize={20} + boxSize={40} + /> + + + ), + }); + }, [navigation, data, model, chasisNumber]); + + // Format currency + const formatCurrency = (amount: number) => { + return `₹${amount.toLocaleString()}`; + }; + + const handleViewAll = () => { + setShowFullHistory(true); + }; + + // Format date + const formatDate = (dateString: string) => { + try { + const date = new Date(dateString); + return date.toLocaleDateString("en-IN", { + day: "2-digit", + month: "short", + year: "numeric", + }); + } catch { + return dateString; + } + }; + + // Get current month/year for header + const getCurrentMonthYear = () => { + const date = new Date(); + return date.toLocaleDateString("en-US", { + month: "long", + year: "numeric", + }); + }; + + const fetchPaymentHistory = async ( + pageNumber: number = 1, + isLoadMore: boolean = false + ) => { + try { + if (isLoadMore) { + setIsLoadingMore(true); + } else { + setIsHistoryLoading(true); + } + + const response = await api.get( + `${BASE_URL}/api/v1/payment-history?page_number=${pageNumber}&page_size=10` + ); + const result: PaymentHistoryResponse = response.data; + + console.log("Payment History Response:", result); + if (result.success) { + const newPayments = result.data.payments; + + if (isLoadMore) { + setPaymentHistory((prev) => [...prev, ...newPayments]); + } else { + setPaymentHistory(newPayments); + } + + // Check if there are more pages + const totalPages = Math.ceil( + result.data.pagination.total_records / + result.data.pagination.page_size + ); + setHasMorePages(pageNumber < totalPages); + setCurrentPage(pageNumber); + } else { + showSnackbar("No payment history found", "error"); + } + } catch (err) { + console.error("Error fetching payment history:", err); + const errorMessage = + err instanceof Error ? err.message : "Something went wrong"; + showSnackbar(errorMessage, "error"); + } finally { + if (isLoadMore) { + setIsLoadingMore(false); + } else { + setIsHistoryLoading(false); + } + } + }; + + const handleLoadMore = () => { + if (hasMorePages && !isLoadingMore) { + fetchPaymentHistory(currentPage + 1, true); + } + }; + + useEffect(() => { + if (isEndReached && showFullHistory && hasMorePages && !isLoadingMore) { + handleLoadMore(); + } + }, [isEndReached]); + + const handleScroll = (event: NativeSyntheticEvent) => { + const { layoutMeasurement, contentOffset, contentSize } = event.nativeEvent; + const paddingToBottom = 20; + const isAtBottom = + layoutMeasurement.height + contentOffset.y >= + contentSize.height - paddingToBottom; + + setIsEndReached(isAtBottom); + }; + + useEffect(() => { + fetchPaymentHistory(1, false); + }, []); + return ( - - Coming Soon... - + + {/* Last EMI Details Card */} + + {/* Header */} + + Last EMI Details + {getCurrentMonthYear()} + + + {/* Divider */} + + + {/* EMI Details Content */} + {true && ( + + + Amount Due + + {emiDetails?.due_amount + ? formatCurrency(emiDetails.due_amount) + : "---"} + + + + + Amount Paid + + {emiDetails?.total_amount_paid_in_current_cycle && + formatCurrency(emiDetails.total_amount_paid_in_current_cycle)} + + + + + Due Date + + {emiDetails?.due_date && emiDetails.due_date} + + + + + Payment Status + + + + )} + + {/* Divider */} + + + {/* Action Buttons */} + + + Pay EMI + + + + View Plan + + + + + + Payment History + + + {isHistoryLoading ? ( + + Loading payment history... + + ) : ( + <> + {/* Show initial payments or all payments based on showFullHistory */} + {(showFullHistory + ? paymentHistory + : paymentHistory.slice(0, 2) + ).map((payment) => ( + + ))} + + {/* View All button */} + {!showFullHistory && paymentHistory.length > 2 && ( + + View all + + + )} + + {/* Loading indicator when fetching more */} + {isLoadingMore && ( + + + Loading more payments... + + + )} + + {/* No more data message */} + {showFullHistory && + !hasMorePages && + paymentHistory.length > 0 && ( + + + No more payments to show + + + )} + + {/* Empty state */} + {paymentHistory.length === 0 && !isHistoryLoading && ( + + + No payment history found + + + )} + + )} + + + + ); } +const StatusBadge = ({ label, type }: { label: string; type: string }) => { + if (!label) return null; + const getBadgeStyle = (type: string) => { + switch (type) { + case "pending": + return { + backgroundColor: "#FFF0E3", + color: "#8E4400", + }; + case "failed": + return { + backgroundColor: "#FDE8E7", + color: "#D51C10", + }; + case "completed": + case "paid": + return { + backgroundColor: "#E8F5E8", + color: "#2D7D32", + }; + default: + return { + backgroundColor: "#E8F5E8", + color: "#2D7D32", + }; + } + }; + + const badgeStyle = getBadgeStyle(type); + + return ( + + + {label.charAt(0).toUpperCase() + label.slice(1)} + + + ); +}; + const styles = StyleSheet.create({ + loadingContainer: { + padding: 16, + alignItems: "center", + justifyContent: "center", + }, container: { flex: 1, alignItems: "center", justifyContent: "center", }, title: { - fontSize: 20, - fontWeight: "bold", + fontSize: 14, + color: "#6B7280", + fontWeight: "500", }, separator: { marginVertical: 30, height: 1, width: "80%", }, + headerTitleContainer: { + flexDirection: "column", + backgroundColor: "#F3F5F8", + }, + subtitle: { + fontSize: 18, + color: "#111827", + fontWeight: "700", + }, + rightContainer: { + flexDirection: "row", + alignItems: "center", + paddingRight: 16, + gap: 8, + backgroundColor: "#F3F5F8", + }, + supportButton: { + backgroundColor: "#F3F5F8", + }, + scrollContainer: { + flex: 1, + paddingHorizontal: 16, + paddingBottom: 16, + width: "100%", + }, + + // EMI Card Styles + emiCard: { + backgroundColor: "#FCFCFC", + borderRadius: 8, + padding: 12, + marginBottom: 36, + }, + cardHeader: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "flex-end", + marginBottom: 12, + }, + headerTitle: { + fontSize: 14, + fontWeight: "600", + color: "#252A34", + }, + headerDate: { + fontSize: 14, + fontWeight: "500", + color: "#565C70", + }, + divider: { + height: 1, + backgroundColor: "#E5E9F0", + marginBottom: 12, + }, + cardContent: { + gap: 8, + marginBottom: 12, + }, + detailRow: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + minHeight: 20, + }, + detailLabel: { + fontSize: 14, + color: "#252A34", + flex: 1, + }, + detailValue: { + fontSize: 14, + fontWeight: "600", + color: "#252A34", + }, + + // Badge Styles + badge: { + paddingHorizontal: 8, + paddingVertical: 2, + borderRadius: 4, + }, + badgeText: { + fontSize: 12, + fontWeight: "500", + }, + + // Button Styles + buttonContainer: { + gap: 8, + }, + primaryButton: { + backgroundColor: "#008761", + borderRadius: 4, + paddingVertical: 8, + paddingHorizontal: 16, + alignItems: "center", + minHeight: 40, + justifyContent: "center", + }, + primaryButtonText: { + color: "#FCFCFC", + fontSize: 14, + fontWeight: "500", + }, + tertiaryButton: { + borderRadius: 4, + paddingVertical: 8, + alignItems: "center", + minHeight: 36, + justifyContent: "center", + }, + tertiaryButtonText: { + color: "#006C4D", + fontSize: 14, + fontWeight: "500", + }, + + // Plan Details Styles + planDetailsSection: { + gap: 8, + marginBottom: 20, + }, + planCard: { + backgroundColor: "#FCFCFC", + borderRadius: 8, + padding: 12, + }, + paymentHistorySection: { + width: 328, + marginLeft: 16, + marginTop: 20, + // No height fixed here, flexible container + }, + sectionTitle: { + fontFamily: "Inter-SemiBold", + fontSize: 14, + lineHeight: 20, + color: "#252A33", // rgb(37,42,51) from design + marginBottom: 8, + }, + paymentHistoryContainer: { + width: 328, + flexDirection: "column", + gap: 16, // for spacing between cards, might need polyfill or use margin + }, + historyLoadingContainer: { + height: 312, + justifyContent: "center", + alignItems: "center", + }, + loadingText: { + fontFamily: "Inter-Regular", + fontSize: 14, + color: "#252A33", + }, + noDataContainer: { + height: 312, + justifyContent: "center", + alignItems: "center", + }, + noDataText: { + fontFamily: "Inter-Regular", + fontSize: 14, + color: "#252A33", + }, + noMoreDataContainer: { + marginTop: 16, + justifyContent: "center", + alignItems: "center", + }, + noMoreDataText: { + fontFamily: "Inter-Regular", + fontSize: 12, + color: "#5B6478", // rgb(91,100,120) muted text + }, + paymentCard: { + width: 328, + height: 76, + backgroundColor: "#FCFCFC", // #FCFCFC or #FBFBFB as in design (near white) + borderRadius: 8, + paddingVertical: 12, + paddingHorizontal: 12, + marginBottom: 16, + justifyContent: "space-between", + // flexDirection: "column" by default + }, + paymentCardTopRow: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + height: 20, + }, + paymentDate: { + fontFamily: "Inter-Regular", + fontSize: 14, + color: "#252A33", + }, + paymentAmount: { + fontFamily: "Inter-SemiBold", + fontSize: 14, + color: "#252A33", + }, + paymentCardBottomRow: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + height: 20, + }, + paymentTimeMethod: { + fontFamily: "Inter-Regular", + fontSize: 12, + color: "#56607A", // rgb(86,96,122) + }, + paymentStatusBadge: { + borderRadius: 4, + paddingVertical: 2, + paddingHorizontal: 8, + flexDirection: "row", + alignItems: "center", + justifyContent: "center", + }, + paymentStatusLabel: { + fontFamily: "Inter-Medium", + fontSize: 12, + textAlign: "center", + }, + // Status colors — you can override backgroundColor and color dynamically based on status + statusPending: { + backgroundColor: "#FFF0E5", // approx. (1, 0.941, 0.89) + color: "#803F0C", // approx. (0.5, 0.27, 0.047) + }, + statusFailed: { + backgroundColor: "#FDDDD7", // approx. (0.99, 0.91, 0.9) + color: "#D6290A", // approx. (0.83, 0.11, 0.06) + }, + statusSuccess: { + backgroundColor: "#E6F4EA", // light green example + color: "#0B8235", + }, + viewAllButton: { + flexDirection: "row", + alignItems: "center", + paddingVertical: 8, + paddingHorizontal: 16, + borderRadius: 4, + }, + viewAllText: { + fontFamily: "Inter-Medium", + fontSize: 14, + color: "#007958", // greenish text color + }, + chevron: { + fontSize: 18, + color: "#007958", + marginLeft: 4, + }, + loadMoreButton: { + paddingVertical: 8, + paddingHorizontal: 16, + borderRadius: 4, + backgroundColor: "#007958", + alignSelf: "center", + }, + loadMoreText: { + fontFamily: "Inter-Medium", + fontSize: 14, + color: "#FFFFFF", + }, }); diff --git a/app/_layout.tsx b/app/_layout.tsx index 1dfe892..ae44bb9 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -11,7 +11,6 @@ import * as SplashScreen from "expo-splash-screen"; import AsyncStorage from "@react-native-async-storage/async-storage"; import { STORAGE_KEYS } from "@/constants/config"; import { setIsLoggedIn } from "@/store/authSlice"; -import { getUserDetails } from "@/store/userSlice"; SplashScreen.preventAutoHideAsync(); diff --git a/assets/icons/refresh.svg b/assets/icons/refresh.svg new file mode 100644 index 0000000..c6051a4 --- /dev/null +++ b/assets/icons/refresh.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/components/Payments/PaymentHistoryCard.tsx b/components/Payments/PaymentHistoryCard.tsx new file mode 100644 index 0000000..405dd15 --- /dev/null +++ b/components/Payments/PaymentHistoryCard.tsx @@ -0,0 +1,152 @@ +import { Text, View } from "react-native"; + +interface PaymentHistoryCardProps { + date: string; + amount: string; + time: string; + method: string; + status: string; +} + +export default ({ + date, + amount, + time, + method, + status, +}: PaymentHistoryCardProps) => { + const getStatusStyle = (status: string) => { + switch (status.toLowerCase()) { + case "failure": + case "failed": + return { + backgroundColor: "#FDE8E7", + color: "#D51C10", + }; + case "pending": + return { + backgroundColor: "#FFF0E3", + color: "#8E4400", + }; + case "confirmed": + case "completed": + case "success": + return { + backgroundColor: "#E8F5E8", + color: "#2D7D32", + }; + default: + return { + backgroundColor: "#E8F5E8", + color: "#2D7D32", + }; + } + }; + + const statusStyle = getStatusStyle(status); + + return ( + + + {date} + {amount} + + + + {time} • {method} + + {status && ( + + + {status.charAt(0).toUpperCase() + status.slice(1)} + + + )} + + + ); +}; + +import { StyleSheet } from "react-native"; + +const styles = StyleSheet.create({ + paymentCard: { + width: 328, + height: 76, + backgroundColor: "rgba(252,252,252,1)", // #FCFCFC + borderRadius: 8, + paddingVertical: 12, + paddingHorizontal: 12, + justifyContent: "flex-start", + alignItems: "flex-start", + }, + paymentCardTop: { + width: 304, + height: 20, + marginLeft: 12, + flexDirection: "row", + justifyContent: "flex-start", + alignItems: "center", + gap: 4, // gap isn't officially supported in RN, so use marginRight on children instead + }, + paymentDate: { + width: 231, + height: 20, + fontFamily: "Inter-Regular", + fontSize: 14, + lineHeight: 20, + color: "rgba(37,42,52,1)", // #252A34 + textAlign: "left", + }, + paymentAmount: { + width: 69, + height: 20, + fontFamily: "Inter-SemiBold", + fontSize: 14, + lineHeight: 20, + color: "rgba(37,42,52,1)", // #252A34 + textAlign: "left", + marginLeft: 4, // simulate gap + }, + paymentCardBottom: { + width: 304, + height: 20, + marginLeft: 12, + flexDirection: "row", + justifyContent: "flex-start", + alignItems: "center", + gap: 4, // simulate gap with margin + }, + paymentDetails: { + width: 237, + height: 16, + fontFamily: "Inter-Regular", + fontSize: 12, + lineHeight: 16, + color: "rgba(86,95,112,1)", // #565F70 + textAlign: "left", + marginTop: 2, // slight vertical offset from Figma y=2 + }, + statusBadge: { + width: 63, + height: 20, + borderRadius: 4, + justifyContent: "center", + alignItems: "center", + paddingHorizontal: 8, + paddingVertical: 2, + marginLeft: 4, // simulate gap + }, + statusText: { + fontFamily: "Inter-Medium", + fontSize: 12, + lineHeight: 16, + color: "#8E4400", // default, overridden by inline style from getStatusStyle + textAlign: "center", + }, +}); diff --git a/components/home/MetricCard.tsx b/components/home/MetricCard.tsx index f7efe62..ce0354e 100644 --- a/components/home/MetricCard.tsx +++ b/components/home/MetricCard.tsx @@ -22,7 +22,7 @@ const MetricCard: React.FC = ({ heading, value, unit }) => { const styles = StyleSheet.create({ container: { - width: 156, + width: "48%", height: 68, backgroundColor: "#FCFCFC", borderRadius: 8, diff --git a/services/axiosClient.ts b/services/axiosClient.ts index dee8a33..e962d6a 100644 --- a/services/axiosClient.ts +++ b/services/axiosClient.ts @@ -24,6 +24,7 @@ api.interceptors.response.use( const status = error.response?.status; //if token is expired or not present, clear it from storage if (status === 401 || status === 403) { + console.log("Token expired or not present"); await AsyncStorage.removeItem(STORAGE_KEYS.AUTH_TOKEN); router.replace("/auth/login"); } diff --git a/services/i18n/locals/en.json b/services/i18n/locals/en.json index 21ecb9d..226dfc8 100644 --- a/services/i18n/locals/en.json +++ b/services/i18n/locals/en.json @@ -3,6 +3,7 @@ "welcome": "Welcome to Driver Saathi", "enter-mobile-number": "Enter Mobile Number", "enter-registered-mobile-number": "Enter your registered mobile number", + "for-any-queries-contact-us": "For any queries, ontact us", "number-not-registered": "Number not registered.", "enter-otp": "Please enter OTP sent to your mobile number", "verify-otp": "Verify OTP", diff --git a/services/socket.ts b/services/socket.ts index 0f3d009..7e63978 100644 --- a/services/socket.ts +++ b/services/socket.ts @@ -6,7 +6,7 @@ import { setTelemetryError, } from "../store/telemetrySlice"; import { BmsState } from "@/constants/types"; -import { BASE_URL, VECTOR_BASE_URL } from "@/constants/config"; +import { BASE_URL, SOCKET_BASE_URL, VECTOR_BASE_URL } from "@/constants/config"; import api from "./axiosClient"; import axios from "axios"; @@ -37,15 +37,15 @@ const fetchToken = async (): Promise => { }; const fetchControllingServer = async (token: string): Promise => { - const hardwareDeviceId = store.getState().user.data?.batteries[0]?.battery_id; + const hardwareDeviceId = store.getState().user.data?.batteries[0]?.device_id; + console.log("Hardware Device ID:", store.getState().user.data); + // const hardwareDeviceId = "VEC16000866082076280974"; try { if (!hardwareDeviceId) throw new Error("Missing hardwareDeviceId"); const response = await axios.get( - `${VECTOR_BASE_URL}/api/device-management/dashboard/get/hardwareDeviceId/${hardwareDeviceId.substring( - 1 - )}`, + `${VECTOR_BASE_URL}/api/device-management/dashboard/get/hardwareDeviceId/${hardwareDeviceId}`, { headers: { Authorization: `Bearer ${token}`, @@ -61,7 +61,8 @@ const fetchControllingServer = async (token: string): Promise => { }; export const connectSocket = () => { - const hardwareDeviceId = store.getState().user.data?.batteries[0]?.battery_id; + // const hardwareDeviceId = store.getState().user.data?.batteries[0]?.battery_id; + const hardwareDeviceId = "V16000866082076280974"; if (!token || !controllingServer || !hardwareDeviceId) { store.dispatch(setTelemetryError("Missing socket auth info")); @@ -72,7 +73,7 @@ export const connectSocket = () => { console.log("Initializing socket connection..."); socket = io( - `${VECTOR_BASE_URL}/?dashboardId=deviceDashboardSocket&assetId=${hardwareDeviceId}`, + `${SOCKET_BASE_URL}/?dashboardId=deviceDashboardSocket&assetId=${hardwareDeviceId}`, { transports: ["websocket"], extraHeaders: { diff --git a/store/paymentSlice.ts b/store/paymentSlice.ts new file mode 100644 index 0000000..e7ebf71 --- /dev/null +++ b/store/paymentSlice.ts @@ -0,0 +1,152 @@ +import { BASE_URL } from "@/constants/config"; +import api from "@/services/axiosClient"; +import { toCamel } from "@/utils/Payments"; +import { createSlice, PayloadAction, createAsyncThunk } from "@reduxjs/toolkit"; +import { AxiosError } from "axios"; + +type LastEmiStatus = "pending" | "completed"; + +export interface MyPlan { + noOfEmi: number | null; + totalAmount: number | null; + downPayment: number | null; + emiAmount: number | null; + totalEmi: number | null; + installmentPaid: number | null; + currentAmount: number | null; +} + +export interface EmiDetails { + dueAmount: number | null; + totalAmountPaidInCurrentCycle: number | null; + dueDate: string | null; + status: LastEmiStatus | null; + advanceBalance: number | null; + pendingCycles: number | null; + totalPendingInstallments: number | null; + myPlan: MyPlan; +} + +type LoadingState = "idle" | "pending" | "succeeded" | "failed"; + +export interface EmiDetailsState { + item?: EmiDetails; + loading: LoadingState; + error?: string; + lastFetchedAt?: number; +} + +const initialState: EmiDetailsState = { + item: undefined, + loading: "idle", + error: undefined, + lastFetchedAt: undefined, +}; + +export interface MyPlanApi { + no_of_emi: number | null; + total_amount: number | null; + down_payment: number | null; + emi_amount: number | null; + total_emi: number | null; + installment_paid: number | null; + current_amount: number | null; +} + +export interface EmiDetailsApi { + due_amount: number | null; + total_amount_paid_in_current_cycle: number | null; + due_date: string | null; + status: LastEmiStatus | null; + advance_balance: number | null; + pending_cycles: number | null; + total_pending_installments: number | null; + myPlain: MyPlanApi; +} + +export interface EmiDetailsResponse { + success: boolean; + data: EmiDetailsApi[]; +} + +const mapEmiDetailsApiToModel = (apiObj: EmiDetailsApi): EmiDetails => ({ + dueAmount: apiObj.due_amount, + totalAmountPaidInCurrentCycle: apiObj.total_amount_paid_in_current_cycle, + dueDate: apiObj.due_date, + status: apiObj.status, + advanceBalance: apiObj.advance_balance, + pendingCycles: apiObj.pending_cycles, + totalPendingInstallments: apiObj.total_pending_installments, + myPlan: { + noOfEmi: apiObj.myPlain.no_of_emi, + totalAmount: apiObj.myPlain.total_amount, + downPayment: apiObj.myPlain.down_payment, + emiAmount: apiObj.myPlain.emi_amount, + totalEmi: apiObj.myPlain.total_emi, + installmentPaid: apiObj.myPlain.installment_paid, + currentAmount: apiObj.myPlain.current_amount, + }, +}); + +export const fetchEmiDetails = createAsyncThunk< + EmiDetails, + void, + { rejectValue: string } +>("emiDetails/fetch", async (_: void, { rejectWithValue }) => { + try { + const res = await api.get(`${BASE_URL}}/api/v1/emi-details`, { + method: "GET", + headers: { "Content-Type": "application/json" }, + }); + + const json = res.data; + + const first = json?.data?.[0]; + if (!json?.success || !first) { + return rejectWithValue("No EMI details found"); + } + + return mapEmiDetailsApiToModel(first); + } catch (e: any) { + const err = e as AxiosError; + if (err.code === "ERR_CANCELED") return rejectWithValue("Request aborted"); + const msg = + err.response?.data?.message || err.message || "Something went wrong"; + return rejectWithValue(msg); + } +}); + +const emiDetailsSlice = createSlice({ + name: "emiDetails", + initialState, + reducers: { + clearEmiDetails(state) { + state.item = undefined; + state.error = undefined; + state.loading = "idle"; + state.lastFetchedAt = undefined; + }, + }, + extraReducers: (builder) => { + builder + .addCase(fetchEmiDetails.pending, (state) => { + state.loading = "pending"; + state.error = undefined; + }) + .addCase( + fetchEmiDetails.fulfilled, + (state, action: PayloadAction) => { + state.loading = "succeeded"; + state.item = action.payload; + state.lastFetchedAt = Date.now(); + } + ) + .addCase(fetchEmiDetails.rejected, (state, action) => { + state.loading = "failed"; + state.error = action.payload as string | undefined; + }); + }, +}); + +export const { clearEmiDetails } = emiDetailsSlice.actions; +export default emiDetailsSlice.reducer; diff --git a/store/rootReducer.ts b/store/rootReducer.ts index e30f68a..b686ce4 100644 --- a/store/rootReducer.ts +++ b/store/rootReducer.ts @@ -2,11 +2,13 @@ import { combineReducers } from "@reduxjs/toolkit"; import authreducer from "./authSlice"; import telemetryReducer from "./telemetrySlice"; import userReducer from "./userSlice"; +import emiDetailsReducer from "./paymentSlice"; const rootReducer = combineReducers({ auth: authreducer, telemetry: telemetryReducer, user: userReducer, + payments: emiDetailsReducer, }); export default rootReducer; diff --git a/store/telemetrySlice.ts b/store/telemetrySlice.ts index c6115b2..f8a347a 100644 --- a/store/telemetrySlice.ts +++ b/store/telemetrySlice.ts @@ -31,6 +31,13 @@ export const telemetrySlice = createSlice({ return { ...state, ...action.payload }; }, setTelemetryLoading: (state) => { + state.loading = true; + state.SoH = null; + state.SoC = null; + state.chargingState = null; + state.lat = null; + state.lon = null; + state.totalDistance = null; state.loading = true; state.error = null; }, diff --git a/store/userSlice.ts b/store/userSlice.ts index 8cdfb0c..f79ce7b 100644 --- a/store/userSlice.ts +++ b/store/userSlice.ts @@ -22,6 +22,7 @@ interface UserData { interface Battery { battery_id: string; + device_id: string; warranty_status: boolean; battery_model: string; bms_id: string; diff --git a/utils/Payments.ts b/utils/Payments.ts new file mode 100644 index 0000000..e4e9503 --- /dev/null +++ b/utils/Payments.ts @@ -0,0 +1,12 @@ +export const toCamel = (obj: any): any => { + if (Array.isArray(obj)) return obj.map(toCamel); + if (obj && typeof obj === "object") { + return Object.fromEntries( + Object.entries(obj).map(([k, v]) => [ + k.replace(/_([a-z])/g, (_, c) => c.toUpperCase()), + toCamel(v), + ]) + ); + } + return obj; +};