Work on Payments screen
parent
7c99b629fc
commit
9d5869c3c2
|
|
@ -18,7 +18,7 @@
|
|||
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/>
|
||||
<meta-data
|
||||
android:name="com.google.android.geo.API_KEY"
|
||||
android:value="AIzaSyBfz20PwTeQkeMonsgXETOViBAy9LOBlXY"/>
|
||||
android:value="AIzaSyCJ4IkKM0ybTOwvdylLVx9BxL2ZV1PEvRk"/>
|
||||
<activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="portrait">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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: () => (
|
||||
<View style={styles.rightContainer}>
|
||||
<Pressable onPress={() => handleManualRefresh()}>
|
||||
<RefreshIcon width={50} height={50} />
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={styles.supportButton}
|
||||
onPress={() => {
|
||||
|
|
@ -115,7 +120,7 @@ export default function HomeScreen() {
|
|||
setIsSupportModalVisible(true);
|
||||
}}
|
||||
>
|
||||
<CustomerCareIcon />
|
||||
<CustomerCareIcon width={50} height={50} />
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
|
|
@ -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 (
|
||||
<View style={{ flex: 1, backgroundColor: "#F3F5F8" }}>
|
||||
<StatusBar style="dark" />
|
||||
|
|
@ -187,6 +204,7 @@ export default function HomeScreen() {
|
|||
longitudeDelta: 0.0421,
|
||||
}}
|
||||
// customMapStyle={mapStyle}
|
||||
userInterfaceStyle="light"
|
||||
>
|
||||
<Marker
|
||||
draggable
|
||||
|
|
@ -384,7 +402,7 @@ const styles = StyleSheet.create({
|
|||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingRight: 16,
|
||||
gap: 8,
|
||||
gap: 4,
|
||||
backgroundColor: "#F3F5F8",
|
||||
},
|
||||
badge: {
|
||||
|
|
|
|||
|
|
@ -1,27 +1,778 @@
|
|||
import { StyleSheet } from "react-native";
|
||||
import { useLayoutEffect, useMemo, useState, useEffect } from "react";
|
||||
import { useNavigation, useRouter } from "expo-router";
|
||||
import {
|
||||
NativeScrollEvent,
|
||||
NativeSyntheticEvent,
|
||||
Pressable,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
} from "react-native";
|
||||
import { Text, View } from "react-native";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "@/store";
|
||||
import ProfileImage from "@/components/home/Profile";
|
||||
import { Overlay } from "@/components/common/Overlay";
|
||||
import { useSnackbar } from "@/contexts/Snackbar";
|
||||
import CustomerCareIcon from "../../assets/icons/customer-care.svg";
|
||||
import api from "@/services/axiosClient";
|
||||
import PaymentHistoryCard from "@/components/Payments/PaymentHistoryCard";
|
||||
import { BASE_URL } from "@/constants/config";
|
||||
|
||||
// Type definitions
|
||||
interface MyPlan {
|
||||
no_of_emi: number;
|
||||
total_amount: number;
|
||||
down_payment: number;
|
||||
emi_amount: number;
|
||||
total_emi: number;
|
||||
installment_paid: number;
|
||||
current_amount: number;
|
||||
}
|
||||
|
||||
interface EmiDetails {
|
||||
due_amount: number;
|
||||
total_amount_paid_in_current_cycle: number;
|
||||
due_date: string;
|
||||
status: string;
|
||||
advance_balance: number;
|
||||
pending_cycles: number;
|
||||
total_pending_installments: number;
|
||||
myPlain: MyPlan;
|
||||
}
|
||||
|
||||
interface EmiResponse {
|
||||
success: boolean;
|
||||
data: EmiDetails[];
|
||||
}
|
||||
|
||||
interface PaymentHistoryItem {
|
||||
id: number;
|
||||
amount: string;
|
||||
status: string;
|
||||
created_at: string;
|
||||
payment_mode: string[];
|
||||
payment_date: string;
|
||||
}
|
||||
|
||||
interface PaymentHistoryPagination {
|
||||
total_records: number;
|
||||
page_number: number;
|
||||
page_size: number;
|
||||
}
|
||||
|
||||
interface PaymentHistoryResponse {
|
||||
success: boolean;
|
||||
data: {
|
||||
payments: PaymentHistoryItem[];
|
||||
pagination: PaymentHistoryPagination;
|
||||
};
|
||||
}
|
||||
|
||||
const formatPaymentDate = (dateString: string) => {
|
||||
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<EmiDetails | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSupportModalVisible, setIsSupportModalVisible] = useState(false);
|
||||
const [isEndReached, setIsEndReached] = useState(false);
|
||||
|
||||
//payment history states
|
||||
const [paymentHistory, setPaymentHistory] = useState<PaymentHistoryItem[]>(
|
||||
[]
|
||||
);
|
||||
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: () => (
|
||||
<View style={styles.headerTitleContainer}>
|
||||
<Text style={styles.title}>{model}</Text>
|
||||
<Text style={styles.subtitle}>{chasisNumber}</Text>
|
||||
</View>
|
||||
),
|
||||
headerRight: () => (
|
||||
<View style={styles.rightContainer}>
|
||||
<Pressable
|
||||
style={styles.supportButton}
|
||||
onPress={() => {
|
||||
console.log("Support Pressed");
|
||||
setIsSupportModalVisible(true);
|
||||
}}
|
||||
>
|
||||
<CustomerCareIcon />
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
router.push("/user/profile");
|
||||
}}
|
||||
>
|
||||
<ProfileImage
|
||||
username={data?.name || "User"}
|
||||
onClick={() => router.push("/user/profile")}
|
||||
textSize={20}
|
||||
boxSize={40}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
),
|
||||
});
|
||||
}, [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<NativeScrollEvent>) => {
|
||||
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 (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>Coming Soon...</Text>
|
||||
</View>
|
||||
<ScrollView
|
||||
style={styles.scrollContainer}
|
||||
showsVerticalScrollIndicator={false}
|
||||
onScroll={handleScroll}
|
||||
scrollEventThrottle={16}
|
||||
>
|
||||
{/* Last EMI Details Card */}
|
||||
<View style={styles.emiCard}>
|
||||
{/* Header */}
|
||||
<View style={styles.cardHeader}>
|
||||
<Text style={styles.headerTitle}>Last EMI Details</Text>
|
||||
<Text style={styles.headerDate}>{getCurrentMonthYear()}</Text>
|
||||
</View>
|
||||
|
||||
{/* Divider */}
|
||||
<View style={styles.divider} />
|
||||
|
||||
{/* EMI Details Content */}
|
||||
{true && (
|
||||
<View style={styles.cardContent}>
|
||||
<View style={styles.detailRow}>
|
||||
<Text style={styles.detailLabel}>Amount Due</Text>
|
||||
<Text style={styles.detailValue}>
|
||||
{emiDetails?.due_amount
|
||||
? formatCurrency(emiDetails.due_amount)
|
||||
: "---"}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.detailRow}>
|
||||
<Text style={styles.detailLabel}>Amount Paid</Text>
|
||||
<Text style={styles.detailValue}>
|
||||
{emiDetails?.total_amount_paid_in_current_cycle &&
|
||||
formatCurrency(emiDetails.total_amount_paid_in_current_cycle)}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.detailRow}>
|
||||
<Text style={styles.detailLabel}>Due Date</Text>
|
||||
<Text style={styles.detailValue}>
|
||||
{emiDetails?.due_date && emiDetails.due_date}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.detailRow}>
|
||||
<Text style={styles.detailLabel}>Payment Status</Text>
|
||||
<StatusBadge
|
||||
label={emiDetails?.status && emiDetails.status}
|
||||
type={emiDetails?.status && emiDetails.status.toLowerCase()}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Divider */}
|
||||
<View style={styles.divider} />
|
||||
|
||||
{/* Action Buttons */}
|
||||
<View style={styles.buttonContainer}>
|
||||
<TouchableOpacity style={styles.primaryButton}>
|
||||
<Text style={styles.primaryButtonText}>Pay EMI</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity style={styles.tertiaryButton}>
|
||||
<Text style={styles.tertiaryButtonText}>View Plan</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.paymentHistorySection}>
|
||||
<Text style={styles.sectionTitle}>Payment History</Text>
|
||||
|
||||
<View style={styles.paymentHistoryContainer}>
|
||||
{isHistoryLoading ? (
|
||||
<View style={styles.historyLoadingContainer}>
|
||||
<Text style={styles.loadingText}>Loading payment history...</Text>
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
{/* Show initial payments or all payments based on showFullHistory */}
|
||||
{(showFullHistory
|
||||
? paymentHistory
|
||||
: paymentHistory.slice(0, 2)
|
||||
).map((payment) => (
|
||||
<PaymentHistoryCard
|
||||
key={payment.id}
|
||||
date={formatPaymentDate(payment.payment_date)}
|
||||
amount={formatCurrency(parseFloat(payment.amount))}
|
||||
time={formatPaymentTime(payment.payment_date)}
|
||||
method={payment.payment_mode.join(", ")}
|
||||
status={payment.status}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* View All button */}
|
||||
{!showFullHistory && paymentHistory.length > 2 && (
|
||||
<TouchableOpacity
|
||||
style={styles.viewAllButton}
|
||||
onPress={handleViewAll}
|
||||
>
|
||||
<Text style={styles.viewAllText}>View all</Text>
|
||||
<Text style={styles.chevron}>›</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
{/* Loading indicator when fetching more */}
|
||||
{isLoadingMore && (
|
||||
<View style={styles.loadingContainer}>
|
||||
<Text style={styles.loadingText}>
|
||||
Loading more payments...
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* No more data message */}
|
||||
{showFullHistory &&
|
||||
!hasMorePages &&
|
||||
paymentHistory.length > 0 && (
|
||||
<View style={styles.noMoreDataContainer}>
|
||||
<Text style={styles.noMoreDataText}>
|
||||
No more payments to show
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Empty state */}
|
||||
{paymentHistory.length === 0 && !isHistoryLoading && (
|
||||
<View style={styles.noDataContainer}>
|
||||
<Text style={styles.noDataText}>
|
||||
No payment history found
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
<Overlay isUploading={isLoading} />
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<View
|
||||
style={[styles.badge, { backgroundColor: badgeStyle.backgroundColor }]}
|
||||
>
|
||||
<Text style={[styles.badgeText, { color: badgeStyle.color }]}>
|
||||
{label.charAt(0).toUpperCase() + label.slice(1)}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
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",
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="mask0_425_4042" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="10" y="10" width="20" height="20">
|
||||
<rect x="10" y="10" width="20" height="20" fill="#D9D9D9"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_425_4042)">
|
||||
<path d="M19.9997 26.6666C18.1386 26.6666 16.5622 26.0208 15.2705 24.7291C13.9788 23.4375 13.333 21.8611 13.333 20C13.333 18.1389 13.9788 16.5625 15.2705 15.2708C16.5622 13.9791 18.1386 13.3333 19.9997 13.3333C20.958 13.3333 21.8747 13.5312 22.7497 13.9271C23.6247 14.3229 24.3747 14.8889 24.9997 15.625V14.1666C24.9997 13.9305 25.0795 13.7326 25.2393 13.5729C25.399 13.4132 25.5969 13.3333 25.833 13.3333C26.0691 13.3333 26.267 13.4132 26.4268 13.5729C26.5865 13.7326 26.6663 13.9305 26.6663 14.1666V18.3333C26.6663 18.5694 26.5865 18.7673 26.4268 18.9271C26.267 19.0868 26.0691 19.1666 25.833 19.1666H21.6663C21.4302 19.1666 21.2323 19.0868 21.0726 18.9271C20.9129 18.7673 20.833 18.5694 20.833 18.3333C20.833 18.0972 20.9129 17.8993 21.0726 17.7396C21.2323 17.5798 21.4302 17.5 21.6663 17.5H24.333C23.8886 16.7222 23.2809 16.1111 22.5101 15.6666C21.7393 15.2222 20.9025 15 19.9997 15C18.6108 15 17.4302 15.4861 16.458 16.4583C15.4858 17.4305 14.9997 18.6111 14.9997 20C14.9997 21.3889 15.4858 22.5694 16.458 23.5416C17.4302 24.5139 18.6108 25 19.9997 25C20.9441 25 21.8087 24.7604 22.5934 24.2812C23.3781 23.8021 23.9858 23.1597 24.4163 22.3541C24.5275 22.1597 24.6837 22.0243 24.8851 21.9479C25.0865 21.8715 25.2913 21.868 25.4997 21.9375C25.7219 22.0069 25.8816 22.1528 25.9788 22.375C26.0761 22.5972 26.0691 22.8055 25.958 23C25.3886 24.1111 24.5761 25 23.5205 25.6666C22.465 26.3333 21.2913 26.6666 19.9997 26.6666Z" fill="#565F70"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
|
|
@ -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 (
|
||||
<View style={styles.paymentCard}>
|
||||
<View style={styles.paymentCardTop}>
|
||||
<Text style={styles.paymentDate}>{date}</Text>
|
||||
<Text style={styles.paymentAmount}>{amount}</Text>
|
||||
</View>
|
||||
<View style={styles.paymentCardBottom}>
|
||||
<Text style={styles.paymentDetails}>
|
||||
{time} • {method}
|
||||
</Text>
|
||||
{status && (
|
||||
<View
|
||||
style={[
|
||||
styles.statusBadge,
|
||||
{ backgroundColor: statusStyle.backgroundColor },
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.statusText, { color: statusStyle.color }]}>
|
||||
{status.charAt(0).toUpperCase() + status.slice(1)}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
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",
|
||||
},
|
||||
});
|
||||
|
|
@ -22,7 +22,7 @@ const MetricCard: React.FC<MetricCardProps> = ({ heading, value, unit }) => {
|
|||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
width: 156,
|
||||
width: "48%",
|
||||
height: 68,
|
||||
backgroundColor: "#FCFCFC",
|
||||
borderRadius: 8,
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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<string> => {
|
|||
};
|
||||
|
||||
const fetchControllingServer = async (token: string): Promise<string> => {
|
||||
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<string>(
|
||||
`${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<string> => {
|
|||
};
|
||||
|
||||
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: {
|
||||
|
|
|
|||
|
|
@ -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<any>;
|
||||
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<EmiDetails>) => {
|
||||
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;
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ interface UserData {
|
|||
|
||||
interface Battery {
|
||||
battery_id: string;
|
||||
device_id: string;
|
||||
warranty_status: boolean;
|
||||
battery_model: string;
|
||||
bms_id: string;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
Loading…
Reference in New Issue