late fee added

This commit is contained in:
Abin Toppo
2025-09-25 16:58:02 +05:30
11 changed files with 750 additions and 159 deletions
+2 -1
View File
@@ -23,9 +23,10 @@ import { RequestCard } from "../../components/RequestCard";
import { useSelector } from "react-redux";
import { UserRequested } from "../../components/UserRequested";
import { RootTabParamList } from "../../navigation/BottomTabs"; // Adjust import path
import { RootStackParamList } from "../../navigation/AppNavigator";
// --- TYPE DEFINITIONS ---
type GroupManagementNavProp = StackNavigationProp<RootTabParamList, 'Management'>;
type GroupManagementNavProp = StackNavigationProp<RootStackParamList, 'ManagementTabs'>;
type ManagementRouteProp = RouteProp<RootTabParamList, 'Management'>;
type ActiveTab = "My Groups" | "Joined Groups" | "Requests" | "Your Requests";
+171 -122
View File
@@ -1,61 +1,91 @@
import React, { useEffect, useState, useCallback } from 'react';
import { View, Text, StyleSheet, ActivityIndicator, ScrollView, RefreshControl, Alert, TouchableOpacity } from 'react-native';
import { useRoute, useNavigation } from '@react-navigation/native';
import * as Keychain from 'react-native-keychain';
import { useRoute, useNavigation, RouteProp } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack';
import { Ionicons } from "@react-native-vector-icons/ionicons";
import colors from '../../components/colors'; // Assuming you have a colors file
import colors from '../../components/colors';
import AppView from '../../components/AppView';
import { apiCall } from '../../utils/ApiHelper';
// --- TYPE DEFINITIONS ---
// Generic API Response Wrapper
interface ApiResponse<T> {
status: 'success' | 'error';
data: T;
message?: string;
statusCode: number;
status: 'success' | 'error';
data: T;
message?: string;
statusCode: number;
}
// Define the shape of your payment details data
interface PaymentDetails {
// Add properties based on your API response, for example:
amount: number;
currency: string;
dueDate: string;
paidBy: string[];
// Type for an individual participant in the payment details
interface Participant {
userId: string;
userName: string;
firstName: string;
lastName: string;
mobile: string;
contributionAmount: number;
paidAmount: number;
status: 'SUCCESS' | 'PENDING';
lateFee: number;
daysLate: number;
lateFeeWaived: boolean;
}
const Payments = () => {
const route = useRoute<any>();
const navigation = useNavigation<any>();
// Fully typed payment details based on your JSON response
interface PaymentDetails {
groupId: string;
groupName: string;
currentCycle: number;
cycleType: string;
contributionAmount: number;
totalContributedAmount: number;
lateFeeMode: string;
lateFeeValue: number;
notReceivedAmount: number;
totalLateFee: number;
totalDueWithLateFee: number;
participants: Participant[];
}
// Assuming your navigation stack params look something like this
type PaymentStackParamList = {
Payments: { groupId: string };
};
type PaymentsRouteProp = RouteProp<PaymentStackParamList, 'Payments'>;
type PaymentsNavigationProp = StackNavigationProp<PaymentStackParamList>;
const Payments: React.FC = () => {
const route = useRoute<PaymentsRouteProp>();
const navigation = useNavigation<PaymentsNavigationProp>();
const { groupId } = route.params;
const [paymentDetails, setPaymentDetails] = useState<any>(null);
const [paymentDetails, setPaymentDetails] = useState<PaymentDetails | null>(null);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const fetchPaymentDetails = useCallback(async () => {
setLoading(true);
try {
// The apiCall function likely handles tokens internally, so we call it directly.
const response = await apiCall(
`https://exotic-humpback-mistakenly.ngrok-free.app/api/payments/current/payment/details/${groupId}`,
'GET'
) as ApiResponse<PaymentDetails>; // Cast the response to the correct type
const fetchPaymentDetails = useCallback(async () => {
if (!refreshing) setLoading(true);
try {
const response = await apiCall(
`https://exotic-humpback-mistakenly.ngrok-free.app/api/payments/current/payment/details/${groupId}`,
'GET'
) as ApiResponse<PaymentDetails>;
console.log("Payment Details Response:", response);
// Check the response and status property for success
if (response && response.statusCode === 200) {
setPaymentDetails(response.data);
} else {
// Use the message from the response object
Alert.alert("Error", response?.message || "Failed to fetch payment details.");
if (response && response.statusCode === 200) {
setPaymentDetails(response.data);
} else {
Alert.alert("Error", response?.message || "Failed to fetch payment details.");
}
} catch (error: any) {
console.error("Fetch payment details error:", error);
Alert.alert("Error", error.message || "An unexpected error occurred.");
} finally {
setLoading(false);
setRefreshing(false);
}
} catch (error: any) {
console.error("Fetch payment details error:", error);
Alert.alert("Error", error.message || "An unexpected error occurred while fetching payment details.");
} finally {
setLoading(false);
setRefreshing(false);
}
}, [groupId]);
}, [groupId, refreshing]);
useEffect(() => {
fetchPaymentDetails();
@@ -63,35 +93,41 @@ const fetchPaymentDetails = useCallback(async () => {
const onRefresh = () => {
setRefreshing(true);
fetchPaymentDetails();
};
const renderParticipant = (participant: any) => (
<View key={participant.userId} style={styles.participantCard}>
<View style={styles.participantInfo}>
<Text style={styles.participantName}>{participant.firstName} {participant.lastName}</Text>
<Text style={styles.participantMobile}>{participant.mobile}</Text>
</View>
<View style={styles.paymentStatus}>
<Text style={[
styles.statusText,
{ color: participant.status === 'PENDING' ? colors.warning : colors.success }
]}>
{participant.status}
</Text>
<Text style={styles.amountText}>
₹{participant.paidAmount} / ₹{participant.contributionAmount}
</Text>
{/* {participant.status === 'PENDING' && (
<TouchableOpacity style={styles.payButton}>
<Text style={styles.payButtonText}>Pay Now</Text>
</TouchableOpacity>
)} */}
</View>
</View>
);
// --- ENHANCED: renderParticipant now shows late fee details ---
const renderParticipant = (participant: Participant) => {
const isPending = participant.status === 'PENDING';
return (
<View key={participant.userId} style={styles.participantCard}>
<View style={styles.participantInfo}>
<Text style={styles.participantName}>{participant.firstName} {participant.lastName}</Text>
<Text style={styles.participantMobile}>{participant.mobile}</Text>
</View>
<View style={styles.paymentStatus}>
<Text style={[
styles.statusText,
{ color: isPending ? '#e0d626ff' : '#18b107ff' }
]}>
{participant.status}
</Text>
<Text style={styles.amountText}>
{isPending ?`₹${participant.payAmount}`:`₹${participant.paidAmount}`}
</Text>
if (loading) {
{/* --- NEW: Conditionally render late payment stats --- */}
{isPending && participant.daysLate > 0 && (
<View style={styles.lateFeeContainer}>
<Text style={styles.lateFeeText}>Late Fee: ₹{participant.lateFee}</Text>
<Text style={styles.lateFeeText}>Days Late: {participant.daysLate}</Text>
</View>
)}
</View>
</View>
);
};
if (loading && !refreshing) {
return <ActivityIndicator style={styles.centered} size="large" color={colors.primary} />;
}
@@ -107,34 +143,34 @@ const fetchPaymentDetails = useCallback(async () => {
}
return (
<AppView>
<ScrollView
style={styles.container}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
>
<View style={styles.header}>
<Text style={styles.headerTitle}>{paymentDetails.groupName}</Text>
</View>
<AppView paddingHorizontal={0}>
<ScrollView
style={styles.container}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} colors={[colors.primary]} tintColor={colors.primary} />}
>
<View style={styles.header}>
<Text style={styles.headerTitle}>{paymentDetails.groupName}</Text>
</View>
<View style={styles.summaryContainer}>
<View style={styles.summaryBox}>
<Text style={styles.summaryLabel}>Current Cycle</Text>
<Text style={styles.summaryValue}>{paymentDetails.currentCycle}</Text>
<View style={styles.summaryContainer}>
<View style={styles.summaryBox}>
<Text style={styles.summaryLabel}>Current Cycle</Text>
<Text style={styles.summaryValue}>{paymentDetails.currentCycle}</Text>
</View>
<View style={styles.summaryBox}>
<Text style={styles.summaryLabel}>Contribution</Text>
<Text style={styles.summaryValue}>₹{paymentDetails.contributionAmount}</Text>
</View>
<View style={styles.summaryBox}>
<Text style={styles.summaryLabel}>Total Due</Text>
<Text style={[styles.summaryValue, { color: colors.danger }]}>₹{paymentDetails.totalDueWithLateFee}</Text>
</View>
</View>
<View style={styles.summaryBox}>
<Text style={styles.summaryLabel}>Contribution</Text>
<Text style={styles.summaryValue}>₹{paymentDetails.contributionAmount}</Text>
</View>
<View style={styles.summaryBox}>
<Text style={styles.summaryLabel}>Total Due</Text>
<Text style={[styles.summaryValue, { color: colors.danger }]}>₹{paymentDetails.totalDueWithLateFee}</Text>
</View>
</View>
<Text style={styles.sectionTitle}>Participants</Text>
{paymentDetails.participants.map(renderParticipant)}
</ScrollView>
</AppView>
<Text style={styles.sectionTitle}>Participants</Text>
{paymentDetails.participants.map(renderParticipant)}
</ScrollView>
</AppView>
);
};
@@ -143,25 +179,19 @@ const styles = StyleSheet.create({
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#f4f6f8',
},
container: {
flex: 1,
backgroundColor: '#f4f6f8',
},
header: {
padding: 20,
flexDirection: 'row',
alignItems: 'center',
borderBottomLeftRadius: 20,
borderBottomRightRadius: 20,
},
backButton: {
marginRight: 15,
paddingBottom: 15,
},
headerTitle: {
color: '#000000ff',
fontSize: 22,
color: '#000000',
fontSize: 24,
fontWeight: 'bold',
},
summaryContainer: {
@@ -169,26 +199,33 @@ const styles = StyleSheet.create({
justifyContent: 'space-around',
padding: 20,
backgroundColor: '#fff',
margin: 15,
borderRadius: 10,
marginHorizontal: 15,
borderRadius: 12,
elevation: 2,
shadowColor: '#000',
shadowOpacity: 0.05,
shadowRadius: 10,
},
summaryBox: {
alignItems: 'center',
},
summaryLabel: {
fontSize: 14,
color: '#666',
color: '#6c757d',
marginBottom: 4,
},
summaryValue: {
fontSize: 18,
fontWeight: 'bold',
fontWeight: '700',
color: colors.primary,
},
sectionTitle: {
fontSize: 18,
fontSize: 20,
fontWeight: 'bold',
marginLeft: 20,
marginTop: 20,
marginBottom: 10,
color: '#343a40',
},
participantCard: {
backgroundColor: '#fff',
@@ -199,17 +236,20 @@ const styles = StyleSheet.create({
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
elevation: 1,
},
participantInfo: {
flex: 1,
},
participantName: {
fontSize: 16,
fontWeight: 'bold',
fontWeight: '600',
color: '#212529',
},
participantMobile: {
fontSize: 14,
color: '#888',
color: '#6c757d',
marginTop: 2,
},
paymentStatus: {
alignItems: 'flex-end',
@@ -217,28 +257,37 @@ const styles = StyleSheet.create({
statusText: {
fontSize: 14,
fontWeight: 'bold',
textTransform: 'uppercase',
},
amountText: {
fontSize: 14,
color: '#495057',
marginTop: 2,
},
payButton: {
marginTop: 5,
backgroundColor: colors.primary,
paddingVertical: 5,
paddingHorizontal: 10,
borderRadius: 5,
// --- NEW STYLES ---
lateFeeContainer: {
marginTop: 8,
paddingTop: 5,
borderTopWidth: 1,
borderTopColor: '#e9ecef',
alignItems: 'flex-end',
},
payButtonText: {
color: '#fff',
fontSize: 12,
lateFeeText: {
fontSize: 13,
color: colors.danger,
fontWeight: '600',
},
retryButton: {
marginTop: 10,
padding: 10,
marginTop: 20,
paddingVertical: 10,
paddingHorizontal: 20,
borderRadius: 8,
backgroundColor: colors.primary,
},
retryButtonText: {
color: colors.primary,
color: '#fff',
fontSize: 16,
fontWeight: 'bold',
},
});