BaaS_Driver_Android_App/app/(tabs)/payments.tsx

779 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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 (
<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: 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",
},
});