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;
+};