payment cycle implementation

This commit is contained in:
Jaanu423
2025-09-29 11:31:48 +05:30
10 changed files with 624 additions and 2163 deletions
+76 -24
View File
@@ -19,7 +19,7 @@ import AppView from '../../components/AppView';
import { Picker } from '@react-native-picker/picker';
import colors from '../../components/colors';
import Clipboard from '@react-native-clipboard/clipboard';
import { CompositeScreenProps } from '@react-navigation/native';
import { CompositeScreenProps, useNavigation } from '@react-navigation/native';
import { BottomTabScreenProps } from '@react-navigation/bottom-tabs';
import { ManagementTabParamList } from './ManagementTabNavigation'; // Adjust path
import { UPLOAD_GROUP_IMAGE } from '../../utils/ApiUrls';
@@ -27,6 +27,8 @@ import { launchImageLibrary } from 'react-native-image-picker';
import *as Keychain from 'react-native-keychain'
import Ionicons from '@react-native-vector-icons/ionicons';
import { CustomButton } from '../../components/common/Custom';
import { StackNavigationProp } from '@react-navigation/stack';
// --- Type Definitions ---
interface Participant {
@@ -37,6 +39,7 @@ interface Participant {
role: 'ORGANIZER' | 'USER';
}
export interface GroupDetails {
id: string;
groupName: string;
@@ -61,6 +64,7 @@ export interface GroupDetails {
imageBlob: string
}
export interface EditGroupPayload {
groupName: string;
description: string;
@@ -74,17 +78,21 @@ export interface EditGroupPayload {
lateFeeValue: number | null;
}
// Correct props for a screen inside a nested Tab Navigator
type EditGroupScreenProps = CompositeScreenProps<
BottomTabScreenProps<ManagementTabParamList, 'EditGroup'>,
NativeStackScreenProps<RootStackParamList>
>;
type EditingCard = 'groupInfo' | 'financialRules' | 'configuration' | null;
type GroupManagementNavProp = StackNavigationProp<RootStackParamList, 'EditGroup'>;
// --- Main Screen Component ---
const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
const { groupId } = route.params;
const navigation = useNavigation<GroupManagementNavProp>();
const [editingCard, setEditingCard] = useState<EditingCard>(null);
const [groupDetails, setGroupDetails] = useState<GroupDetails | null>(null);
@@ -106,10 +114,11 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
const [starting, setStarting] = useState(false);
const [inviting, setInviting] = useState(false);
const [error, setError] = useState<string | null>(null);
const [paymentStatus, setPaymentStatus] = useState<boolean | null>(null);
const [currentCycle, setCurrentCycle] = useState<number | null>(null);
const [selectedImage, setSelectedImage] = useState<string | null>()
const [imageUpdating, setImageUpdating] = useState<boolean>(false)
// ... (All your handler functions: initializeEditableData, fetchGroupDetails, handleCancel, handleSave, etc. remain the same)
const initializeEditableData = (data: GroupDetails) => {
setEditableData({
groupName: data.groupName,
@@ -125,6 +134,7 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
});
};
const fetchGroupDetails = useCallback(async () => {
setLoading(true);
setError(null);
@@ -143,11 +153,22 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
setLoading(false);
}
}, [groupId]);
const getPaymentStatus = async()=>{
const response = await apiCall(`https://exotic-humpback-mistakenly.ngrok-free.app/api/payments/group/${groupId}/payment/paid`, 'GET') as { status: string; data?: { currentCycle: number ,status: boolean }; message?: string };
console.log("paymentStatus",response)
if (response.status === 'success') {
setPaymentStatus(response.data?.status || false);
setCurrentCycle(response.data?.currentCycle || null);
}
}
useEffect(() => {
fetchGroupDetails();
getPaymentStatus();
}, [fetchGroupDetails]);
const handleCancel = () => {
if (groupDetails) {
initializeEditableData(groupDetails);
@@ -155,9 +176,11 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
setEditingCard(null);
};
const handleSave = async () => {
if (!groupDetails) return;
if (editableData.maxParticipants < groupDetails.totalParticipants) {
Alert.alert('Invalid Input', `Max participants cannot be less than the current count (${groupDetails.totalParticipants}).`);
return;
@@ -175,10 +198,14 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
setSaving(false);
}
};
const handleDistribute = async () => {
navigation.navigate('RandomSelectUserScreen', { groupId: groupDetails?.id || '', cycleNumber: currentCycle || 0 });
};
const handleStartGroup = async () => {
if (!groupDetails) return;
Alert.alert(
"Confirm Start",
"Are you sure you want to start this group? This action cannot be undone.",
@@ -208,7 +235,8 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
if (!groupDetails) return;
setInviting(true);
try {
const response = await apiCall(`https://exotic-humpback-mistakenly.ngrok-free.app/api/invite/generate?groupId=${groupDetails.id}`, 'GET') as { data?: string };
const response = await apiCall(`https://exotic-humpback-mistakenly.ngrok-free.app/api/invite/generate?groupId=${groupDetails.id}`, 'GET') as { data?: string };
const inviteLink = response.data;
@@ -230,12 +258,13 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
}
};
const handleUpdateField = (field: keyof EditGroupPayload, value: any) => {
setEditableData(prev => ({ ...prev, [field]: value }));
};
// ... (renderField and renderCard functions also remain the same)
const renderField = (field: keyof EditGroupPayload, label: string, displayValue: string | number, isEditing: boolean) => {
const renderField = (field: keyof EditGroupPayload, label: string, displayValue: string | number, isEditing: boolean, isEditable: boolean) => {
const commonInputProps = { style: styles.input, placeholderTextColor: "#9CA3AF" };
const getInputForField = () => {
@@ -286,6 +315,7 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
const renderCard = (title: string, fields: Array<{ field: keyof EditGroupPayload, label: string, displayValue: () => string | number }>, cardKey: EditingCard) => {
const isEditingThisCard = editingCard === cardKey;
return (
<View style={styles.card}>
<View style={styles.cardHeader}>
@@ -319,6 +349,7 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
if (error) return <View style={styles.centered}><Text style={styles.errorText}>{error}</Text></View>;
if (!groupDetails) return null;
const isEditable = groupDetails.status === 'PENDING' || groupDetails.status === 'APPROVED';
const isInvitable = groupDetails.status === 'APPROVED';
const canStartGroup = groupDetails.status === 'APPROVED' && groupDetails.totalParticipants >= groupDetails.minParticipants;
@@ -447,7 +478,6 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
return (
// Use AppView as the root container with flex: 1
<AppView style={{ flex: 1, paddingHorizontal: 0 }}>
<View style={styles.headerContainer}>
<Text style={styles.headerTitle}>Manage Group</Text>
@@ -458,6 +488,7 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
)}
</View>
<ScrollView
style={styles.container}
contentContainerStyle={styles.contentContainer}
@@ -490,20 +521,29 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
{ field: 'description', label: 'Description', displayValue: () => groupDetails.description },
], 'groupInfo')}
{renderCard('Financial Rules', [
{ field: 'contributionAmount', label: 'Contribution', displayValue: () => `₹${groupDetails.contributionAmount.toLocaleString('en-IN')}` },
{ field: 'maxParticipants', label: 'Max Participants', displayValue: () => `${groupDetails.totalParticipants} / ${groupDetails.maxParticipants}` },
{ field: 'minParticipants', label: 'Min Participants', displayValue: () => `${groupDetails.minParticipants}` },
{ field: 'latePaymentCharge', label: 'Late Fee', displayValue: () => `₹${(groupDetails.latePaymentCharge ?? 0).toLocaleString('en-IN')}` },
], 'financialRules')}
{groupDetails.status === 'ACTIVE' ?
renderCard('Financial Rules', [
{ field: 'contributionAmount', label: 'Contribution', displayValue: () => `₹${groupDetails.contributionAmount.toLocaleString('en-IN')}` },
{ field: 'latePaymentCharge', label: 'Late Fee', displayValue: () => `₹${(groupDetails.latePaymentCharge ?? 0).toLocaleString('en-IN')}` },
{ field: 'maxParticipants', label: 'Current Participants', displayValue: () => `${groupDetails.totalParticipants}` },
], 'financialRules')
:
renderCard('Financial Rules', [
{ field: 'contributionAmount', label: 'Contribution', displayValue: () => `₹${groupDetails.contributionAmount.toLocaleString('en-IN')}` },
{ field: 'maxParticipants', label: 'Max Participants', displayValue: () => `${groupDetails.totalParticipants} / ${groupDetails.maxParticipants}` },
{ field: 'minParticipants', label: 'Min Participants', displayValue: () => `${groupDetails.minParticipants}` },
{ field: 'latePaymentCharge', label: 'Late Fee', displayValue: () => `₹${(groupDetails.latePaymentCharge ?? 0).toLocaleString('en-IN')}` },
], 'financialRules')
}
{renderCard('Configuration', [
{ field: 'cycleType', label: 'Payment Cycle', displayValue: () => groupDetails.cycleType },
{ field: 'customCycleDays', label: 'Custom Days', displayValue: () => `${groupDetails.customCycleDays ?? ''} days` },
...(groupDetails.cycleType === 'CUSTOM' ? [{ field: 'customCycleDays' as keyof EditGroupPayload, label: 'Custom Days', displayValue: () => `${groupDetails.customCycleDays} days` }] : []),
{ field: 'visibility', label: 'Visibility', displayValue: () => groupDetails.visibility },
], 'configuration')}
{/* Participants Card ... */}
<View style={styles.card}>
<Text style={styles.cardTitle}>Participants ({groupDetails.totalParticipants})</Text>
{groupDetails.participants.length > 0 ? (
@@ -524,8 +564,7 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
</View>
</ScrollView>
{/* --- FLOATING BUTTON AREA --- */}
{/* This View is a sibling of ScrollView, allowing it to float on top */}
{canStartGroup && (
<View style={styles.floatingButtonContainer}>
<TouchableOpacity
@@ -541,14 +580,28 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
</TouchableOpacity>
</View>
)}
{paymentStatus && (
<View style={styles.floatingButtonContainer}>
<TouchableOpacity
style={styles.activateButton}
onPress={handleDistribute}
disabled={starting}
>
{starting ? (
<ActivityIndicator color="#FFFFFF" size="small" />
) : (
<Text style={styles.activateButtonText}>Distribute</Text>
)}
</TouchableOpacity>
</View>
)}
</AppView>
);
};
// --- Styles ---
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#F3F4F6' },
// Added more paddingBottom to ensure content doesn't hide behind the button AND the tab bar
contentContainer: { padding: 16, paddingTop: 0, paddingBottom: 150 },
centered: { flex: 1, justifyContent: 'center', alignItems: 'center' },
errorText: { color: '#EF4444', fontSize: 16, textAlign: 'center' },
@@ -619,15 +672,13 @@ const styles = StyleSheet.create({
cancelButton: { backgroundColor: '#E5E7EB' },
inviteButton: { backgroundColor: colors.primary },
buttonText: { color: '#ffffffff', fontSize: 15, fontWeight: '600' },
// --- FLOATING BUTTON STYLES ---
floatingButtonContainer: {
position: 'absolute',
bottom: 85, // Positioned above the tab bar
bottom: 85,
left: 20,
right: 20,
alignItems: 'center', // Center the button horizontally
zIndex: 1, // Ensure it's on top
alignItems: 'center',
zIndex: 1,
},
activateButton: {
backgroundColor: colors.pastelOrange,
@@ -639,7 +690,7 @@ const styles = StyleSheet.create({
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 4,
width: '90%', // Adjust width as needed
width: '90%',
alignItems: 'center',
},
activateButtonText: {
@@ -649,4 +700,5 @@ const styles = StyleSheet.create({
},
});
export default EditGroupScreen;