Merge branch 'feature_profile' of https://git.zeptic.in/abintoppo_lincpay/CircleCollect into feature_pending
This commit is contained in:
@@ -46,6 +46,7 @@ const styles = StyleSheet.create({
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
paddingVertical: 12,
|
paddingVertical: 12,
|
||||||
|
paddingHorizontal:12
|
||||||
},
|
},
|
||||||
headerTitle: {
|
headerTitle: {
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
|
|||||||
@@ -0,0 +1,317 @@
|
|||||||
|
import React, { useState, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
View,
|
||||||
|
FlatList,
|
||||||
|
TouchableOpacity,
|
||||||
|
LayoutAnimation,
|
||||||
|
UIManager,
|
||||||
|
Platform,
|
||||||
|
} from 'react-native';
|
||||||
|
import { Ionicons } from "@react-native-vector-icons/ionicons";import colors from "../../../components/colors"; // Assuming a colors file
|
||||||
|
|
||||||
|
// Enable LayoutAnimation for Android
|
||||||
|
if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
|
||||||
|
UIManager.setLayoutAnimationEnabledExperimental(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- TypeScript Interfaces for Type Safety ---
|
||||||
|
|
||||||
|
interface Contribution {
|
||||||
|
transactionId: string;
|
||||||
|
userName: string;
|
||||||
|
baseAmount: number;
|
||||||
|
lateFee: number;
|
||||||
|
waivedAmount: number;
|
||||||
|
totalAmount: number;
|
||||||
|
status: 'SUCCESS' | 'PENDING' | 'FAILED';
|
||||||
|
paidDate: [number, number, number];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Withdrawal {
|
||||||
|
transactionId: string;
|
||||||
|
userName: string;
|
||||||
|
baseAmount: number;
|
||||||
|
totalAmount: number;
|
||||||
|
status: 'SUCCESS' | 'PENDING' | 'FAILED';
|
||||||
|
paidDate: [number, number, number];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Cycle {
|
||||||
|
cycleNumber: number;
|
||||||
|
cycleStart: [number, number, number];
|
||||||
|
cycleEnd: [number, number, number];
|
||||||
|
expectedContribution: number;
|
||||||
|
contributions: Contribution[];
|
||||||
|
withdrawal: Withdrawal | null;
|
||||||
|
totalParticipants: number;
|
||||||
|
totalCollected: number;
|
||||||
|
collectionPercentage: number;
|
||||||
|
paidParticipants: number;
|
||||||
|
pendingParticipants: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CycleDashboardData {
|
||||||
|
groupId: string;
|
||||||
|
groupName: string;
|
||||||
|
totalCycles: number;
|
||||||
|
currentCycle: number;
|
||||||
|
contributionAmount: number;
|
||||||
|
cycleType: 'MONTHLY' | 'WEEKLY' | 'DAILY';
|
||||||
|
status: 'COMPLETED' | 'ACTIVE' | 'PENDING';
|
||||||
|
cycles: Cycle[];
|
||||||
|
totalParticipants: number;
|
||||||
|
totalCollected: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --- Helper Functions ---
|
||||||
|
|
||||||
|
const formatDate = (dateArray: [number, number, number]): string => {
|
||||||
|
if (!dateArray || dateArray.length < 3) return 'N/A';
|
||||||
|
const [year, month, day] = dateArray;
|
||||||
|
return new Date(year, month - 1, day).toLocaleDateString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatCurrency = (amount: number): string => {
|
||||||
|
return new Intl.NumberFormat('en-IN', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'INR',
|
||||||
|
minimumFractionDigits: 0,
|
||||||
|
}).format(amount);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// --- Child Components ---
|
||||||
|
|
||||||
|
const CycleItem = ({ item, isExpanded, onToggle }: { item: Cycle, isExpanded: boolean, onToggle: () => void }) => {
|
||||||
|
return (
|
||||||
|
<View style={styles.cycleCard}>
|
||||||
|
<TouchableOpacity onPress={onToggle} style={styles.cycleHeader}>
|
||||||
|
<View>
|
||||||
|
<Text style={styles.cycleTitle}>Cycle {item.cycleNumber}</Text>
|
||||||
|
<Text style={styles.cycleDateRange}>
|
||||||
|
{formatDate(item.cycleStart)} - {formatDate(item.cycleEnd)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Ionicons name={isExpanded ? 'chevron-up' : 'chevron-down'} size={24} color={colors.black} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
{isExpanded && (
|
||||||
|
<View style={styles.cycleDetails}>
|
||||||
|
{/* Withdrawal Info */}
|
||||||
|
{item.withdrawal && (
|
||||||
|
<View style={styles.section}>
|
||||||
|
<Text style={styles.sectionTitle}>Withdrawal</Text>
|
||||||
|
<View style={styles.withdrawalCard}>
|
||||||
|
<Ionicons name="arrow-down-circle-outline" size={32} color={colors.Home} />
|
||||||
|
<View style={styles.withdrawalInfo}>
|
||||||
|
<Text style={styles.withdrawalUser}>{item.withdrawal.userName}</Text>
|
||||||
|
<Text style={styles.withdrawalAmount}>{formatCurrency(item.withdrawal.totalAmount)}</Text>
|
||||||
|
</View>
|
||||||
|
<Text style={styles.withdrawalDate}>{formatDate(item.withdrawal.paidDate)}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Contributions List */}
|
||||||
|
<View style={styles.section}>
|
||||||
|
<Text style={styles.sectionTitle}>Contributions ({item.paidParticipants}/{item.totalParticipants})</Text>
|
||||||
|
{item.contributions.map(contribution => (
|
||||||
|
<View key={contribution.transactionId} style={styles.contributionItem}>
|
||||||
|
<Ionicons name="person-circle-outline" size={28} color="#475569" />
|
||||||
|
<View style={{ flex: 1, marginLeft: 10 }}>
|
||||||
|
<Text style={styles.contributorName}>{contribution.userName}</Text>
|
||||||
|
<Text style={styles.contributionDate}>{formatDate(contribution.paidDate)}</Text>
|
||||||
|
</View>
|
||||||
|
<Text style={styles.contributionAmount}>{formatCurrency(contribution.totalAmount)}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// --- Main Dashboard Component ---
|
||||||
|
|
||||||
|
const CycleDashboard = ({ data }: { data: CycleDashboardData }) => {
|
||||||
|
const [expandedCycle, setExpandedCycle] = useState<number | null>(data.currentCycle);
|
||||||
|
|
||||||
|
const handleToggle = (cycleNumber: number) => {
|
||||||
|
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
|
||||||
|
setExpandedCycle(expandedCycle === cycleNumber ? null : cycleNumber);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderHeader = () => (
|
||||||
|
<View style={styles.headerContainer}>
|
||||||
|
<Text style={styles.groupName}>{data.groupName}</Text>
|
||||||
|
<View style={styles.summaryRow}>
|
||||||
|
<View style={styles.summaryBox}>
|
||||||
|
<Text style={styles.summaryLabel}>Total Collected</Text>
|
||||||
|
<Text style={styles.summaryValue}>{formatCurrency(data.totalCollected)}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.summaryBox}>
|
||||||
|
<Text style={styles.summaryLabel}>Status</Text>
|
||||||
|
<Text style={[styles.summaryValue, { color: data.status === 'COMPLETED' ? colors.Settings : colors.Home }]}>{data.status}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.summaryBox}>
|
||||||
|
<Text style={styles.summaryLabel}>Cycles</Text>
|
||||||
|
<Text style={styles.summaryValue}>{data.currentCycle}/{data.totalCycles}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FlatList
|
||||||
|
data={data.cycles}
|
||||||
|
keyExtractor={(item) => item.cycleNumber.toString()}
|
||||||
|
renderItem={({ item }) => (
|
||||||
|
<CycleItem
|
||||||
|
item={item}
|
||||||
|
isExpanded={expandedCycle === item.cycleNumber}
|
||||||
|
onToggle={() => handleToggle(item.cycleNumber)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
ListHeaderComponent={renderHeader}
|
||||||
|
contentContainerStyle={styles.container}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CycleDashboard;
|
||||||
|
|
||||||
|
|
||||||
|
// --- Styles ---
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
paddingHorizontal: 15,
|
||||||
|
paddingBottom: 20,
|
||||||
|
},
|
||||||
|
// Header
|
||||||
|
headerContainer: {
|
||||||
|
marginBottom: 20,
|
||||||
|
},
|
||||||
|
groupName: {
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: '#1e293b',
|
||||||
|
marginBottom: 15,
|
||||||
|
},
|
||||||
|
summaryRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
},
|
||||||
|
summaryBox: {
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
summaryLabel: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: '#64748b',
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
summaryValue: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: '#0f172a',
|
||||||
|
},
|
||||||
|
// Cycle Item Card
|
||||||
|
cycleCard: {
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
borderRadius: 12,
|
||||||
|
marginBottom: 15,
|
||||||
|
elevation: 2,
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOpacity: 0.08,
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowRadius: 8,
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
|
cycleHeader: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: 16,
|
||||||
|
},
|
||||||
|
cycleTitle: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: '#1e293b',
|
||||||
|
},
|
||||||
|
cycleDateRange: {
|
||||||
|
fontSize: 13,
|
||||||
|
color: '#64748b',
|
||||||
|
marginTop: 2,
|
||||||
|
},
|
||||||
|
cycleDetails: {
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingBottom: 16,
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: '#f1f5f9',
|
||||||
|
},
|
||||||
|
section: {
|
||||||
|
marginTop: 16,
|
||||||
|
},
|
||||||
|
sectionTitle: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: '#334155',
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
// Withdrawal Card
|
||||||
|
withdrawalCard: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: colors.pastelPink, // Example color
|
||||||
|
padding: 12,
|
||||||
|
borderRadius: 8,
|
||||||
|
},
|
||||||
|
withdrawalInfo: {
|
||||||
|
flex: 1,
|
||||||
|
marginLeft: 12,
|
||||||
|
},
|
||||||
|
withdrawalUser: {
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: colors.Home
|
||||||
|
},
|
||||||
|
withdrawalAmount: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: colors.Home
|
||||||
|
},
|
||||||
|
withdrawalDate: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: '#475569'
|
||||||
|
},
|
||||||
|
// Contribution Item
|
||||||
|
contributionItem: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingVertical: 10,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: '#f1f5f9',
|
||||||
|
},
|
||||||
|
contributorName: {
|
||||||
|
fontSize: 15,
|
||||||
|
color: '#1e293b',
|
||||||
|
},
|
||||||
|
contributionDate: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: '#64748b',
|
||||||
|
},
|
||||||
|
contributionAmount: {
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: '500',
|
||||||
|
color: '#1e293b',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
import React, { useRef, useEffect, useState } from "react";
|
import React, { useEffect, useState, useCallback } from "react";
|
||||||
import {
|
import {
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Text,
|
Text,
|
||||||
View,
|
View,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
FlatList,
|
|
||||||
Animated,
|
|
||||||
ToastAndroid,
|
ToastAndroid,
|
||||||
Alert,
|
RefreshControl,
|
||||||
|
ScrollView,
|
||||||
|
Modal,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { useRoute, RouteProp } from "@react-navigation/native";
|
import { useRoute } from "@react-navigation/native";
|
||||||
import AppView from "../../../components/AppView";
|
import AppView from "../../../components/AppView";
|
||||||
import colors from "../../../components/colors";
|
import colors from "../../../components/colors";
|
||||||
import { Ionicons } from "@react-native-vector-icons/ionicons";
|
import { Ionicons } from "@react-native-vector-icons/ionicons";
|
||||||
@@ -17,30 +17,62 @@ import Header from "../../../components/common/Header";
|
|||||||
import { apiCall } from "../../../utils/ApiHelper";
|
import { apiCall } from "../../../utils/ApiHelper";
|
||||||
import { BASE_URL, CONTRIBUTE, GROUP_ALL_MEMBER } from "../../../utils/ApiUrls";
|
import { BASE_URL, CONTRIBUTE, GROUP_ALL_MEMBER } from "../../../utils/ApiUrls";
|
||||||
import ContributionScreen from "../../ContributionScreen";
|
import ContributionScreen from "../../ContributionScreen";
|
||||||
|
import CycleDashboard from "./CycleDashboard";
|
||||||
|
|
||||||
// Define type for a member
|
const CustomDropdown = ({ items, selectedValue, onValueChange, placeholder = "Select an item" }:any) => {
|
||||||
interface Member {
|
const [open, setOpen] = useState(false);
|
||||||
id: string;
|
const selectedLabel = items.find((item:any) => item.value === selectedValue)?.label || placeholder;
|
||||||
name: string;
|
|
||||||
role: "Organizer" | "Member";
|
|
||||||
contribution: number;
|
|
||||||
data: any
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define route params type
|
return (
|
||||||
|
<View style={styles.dropdownWrapper}>
|
||||||
|
<TouchableOpacity onPress={() => setOpen(true)} style={styles.dropdownHeader}>
|
||||||
|
<Text style={styles.headerText}>{selectedLabel}</Text>
|
||||||
|
<Ionicons name='chevron-down' size={22} color={colors.black} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
transparent
|
||||||
|
animationType="fade"
|
||||||
|
visible={open}
|
||||||
|
onRequestClose={() => setOpen(false)}
|
||||||
|
>
|
||||||
|
<TouchableOpacity style={styles.modalOverlay} onPress={() => setOpen(false)}>
|
||||||
|
<View style={styles.dropdownListContainer}>
|
||||||
|
<ScrollView>
|
||||||
|
{items.map(item => (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={item.value}
|
||||||
|
onPress={() => {
|
||||||
|
onValueChange(item.value);
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
style={[
|
||||||
|
styles.dropdownItem,
|
||||||
|
item.value === selectedValue && styles.selectedItem
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text style={styles.itemText}>{item.label}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</Modal>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const GroupDetails = () => {
|
const GroupDetails = () => {
|
||||||
const route = useRoute<any>();
|
const route = useRoute<any>();
|
||||||
const { id, groupName, description, contributionAmount, currentParticipants } = route.params;
|
const [memberData, setMemberData] = useState([]);
|
||||||
|
|
||||||
const [memberData, setMemberData] = useState<Member[]>([]);
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [openModal, setOpenModal] = useState(false)
|
const [openModal, setOpenModal] = useState(false);
|
||||||
const [payable, setPayable] = useState() as any
|
const [payable, setPayable] = useState<any>();
|
||||||
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
const [cycleDashboardData, setCycleDashboardData] = useState<any>();
|
||||||
|
const [selectedCycleNumber, setSelectedCycleNumber] = useState(null);
|
||||||
|
const [cycleItems, setCycleItems] = useState([]);
|
||||||
|
|
||||||
console.log("route param ggggggggggg : ", route?.params)
|
|
||||||
// Group object
|
|
||||||
const group = {
|
const group = {
|
||||||
id: route.params.item?.id,
|
id: route.params.item?.id,
|
||||||
name: route.params.item?.groupName,
|
name: route.params.item?.groupName,
|
||||||
@@ -51,224 +83,291 @@ const GroupDetails = () => {
|
|||||||
members: memberData,
|
members: memberData,
|
||||||
};
|
};
|
||||||
|
|
||||||
const progress = Math.min(group.collected / group.goal, 1);
|
const getCycleDashboard = async () => {
|
||||||
|
try {
|
||||||
|
const response = await apiCall(`${BASE_URL}payments/groups/${group.id}/cycle-dashboard`, "GET") as any;
|
||||||
|
const responseData = response.data;
|
||||||
|
setCycleDashboardData(responseData);
|
||||||
|
|
||||||
// Animation refs
|
if (responseData?.cycles) {
|
||||||
const animatedValues = useRef<Animated.Value[]>([]);
|
const items = responseData.cycles.map(cycle => ({
|
||||||
|
label: `Cycle ${cycle.cycleNumber}`,
|
||||||
// useEffect(() => {
|
value: cycle.cycleNumber
|
||||||
// animatedValues.current = group.members.map(() => new Animated.Value(0));
|
}));
|
||||||
// }, [group.members]);
|
setCycleItems(items);
|
||||||
|
if (responseData.currentCycle) {
|
||||||
// useEffect(() => {
|
setSelectedCycleNumber(responseData.currentCycle);
|
||||||
// const animations = group.members.map((_, i) =>
|
}
|
||||||
// Animated.timing(animatedValues.current[i], {
|
|
||||||
// toValue: 1,
|
|
||||||
// duration: 400,
|
|
||||||
// delay: i * 100,
|
|
||||||
// useNativeDriver: true,
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
// Animated.stagger(100, animations).start();
|
|
||||||
// }, [group.members]);
|
|
||||||
|
|
||||||
// Fetch group members
|
|
||||||
useEffect(() => {
|
|
||||||
const handleGroupDetails = async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
const res = await apiCall(`${GROUP_ALL_MEMBER}?groupId=${route?.params?.item?.id}`, "GET") as any;
|
|
||||||
console.log("Group details response:", res);
|
|
||||||
setMemberData(res.data);
|
|
||||||
ToastAndroid.show("Group details fetched successfully!", ToastAndroid.SHORT);
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error("Error fetching group details:", error);
|
|
||||||
ToastAndroid.show(error?.message || "Failed to fetch group details", ToastAndroid.SHORT);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
};
|
} catch (error) {
|
||||||
|
ToastAndroid.show("Failed to fetch cycle data", ToastAndroid.SHORT);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
handleGroupDetails();
|
const fetchGroupDetails = async () => {
|
||||||
}, [id]);
|
try {
|
||||||
|
const res = await apiCall(`${GROUP_ALL_MEMBER}?groupId=${group.id}`, "GET") as any;
|
||||||
|
console.log("Group Members Response: ", res);
|
||||||
|
setMemberData(res.data);
|
||||||
|
} catch (error: any) {
|
||||||
|
ToastAndroid.show(error?.message || "Failed to fetch group details", ToastAndroid.SHORT);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleContribution = async () => {
|
const handleContribution = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await apiCall(`${CONTRIBUTE}${group.id}/calculate-contribution`, 'GET') as any
|
const res = await apiCall(`${CONTRIBUTE}${group.id}/calculate-contribution`, 'GET') as any;
|
||||||
setPayable(res.data)
|
setPayable(res.data);
|
||||||
console.log("contribution res : ", res)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("contribution error : ", error)
|
console.log("Error in handleContribution: ", error);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
// useEffect(()=>{
|
const fetchAllData = useCallback(async () => {
|
||||||
// if(group?.id)
|
setLoading(true);
|
||||||
// handleContribution()
|
await Promise.all([
|
||||||
// },[])
|
fetchGroupDetails(),
|
||||||
|
handleContribution(),
|
||||||
|
getCycleDashboard()
|
||||||
|
]);
|
||||||
|
setLoading(false);
|
||||||
|
}, [group.id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (group.id) {
|
||||||
|
fetchAllData();
|
||||||
|
}
|
||||||
|
}, [group.id, fetchAllData]);
|
||||||
|
|
||||||
|
const onRefresh = useCallback(async () => {
|
||||||
|
setRefreshing(true);
|
||||||
|
await fetchAllData();
|
||||||
|
setRefreshing(false);
|
||||||
|
}, [fetchAllData]);
|
||||||
|
|
||||||
const confirmPayment = async () => {
|
const confirmPayment = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await apiCall(`${BASE_URL}payments/contribute/${group?.id}?amount=${payable?.totalPayable}`, 'POST') as any
|
const res = await apiCall(`${BASE_URL}payments/contribute/${group?.id}?amount=${payable?.totalPayable}`, 'POST') as any;
|
||||||
console.log("payment response : ", res)
|
if (res.status === 200 || res.status === 201) {
|
||||||
if (res.status) {
|
ToastAndroid.show('Payment done!', ToastAndroid.SHORT);
|
||||||
ToastAndroid.show('Payment done!', ToastAndroid.SHORT)
|
onRefresh();
|
||||||
}
|
} else {
|
||||||
if (res.status === 400) {
|
ToastAndroid.show(res?.data?.message || 'Payment failed!', ToastAndroid.SHORT);
|
||||||
ToastAndroid.show(res?.data?.message || 'Payment done!', ToastAndroid.SHORT)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("payment error : ", error)
|
ToastAndroid.show("An error occurred during payment.", ToastAndroid.SHORT);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedCycleData = cycleDashboardData?.cycles.find(c => c.cycleNumber === selectedCycleNumber);
|
||||||
}
|
|
||||||
const renderMember = ({ item, index }: { item: any; index: number }) => (
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
styles.memberCard,
|
|
||||||
{
|
|
||||||
// opacity: animatedValues.current[index],
|
|
||||||
// transform: [
|
|
||||||
// {
|
|
||||||
// translateY: animatedValues.current[index].interpolate({
|
|
||||||
// inputRange: [0, 1],
|
|
||||||
// outputRange: [20, 0],
|
|
||||||
// }),
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
borderColor: route.params?.color ? route.params?.color : colors.Home
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<View style={{ flexDirection: "row", gap: 10 }}>
|
|
||||||
<Ionicons name="person-circle-outline" size={40} color={route.params?.color ? route.params?.color : colors.primary} />
|
|
||||||
<View>
|
|
||||||
<Text style={styles.memberName}>
|
|
||||||
{item.firstName} {item.lastName} {item.role === "Organizer" ? "(Organizer)" : "(Member)"}
|
|
||||||
</Text>
|
|
||||||
<Text style={styles.memberDetail}>Mobile: {item.mobile}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppView>
|
<AppView paddingHorizontal={0} style={{ flex: 1 }}>
|
||||||
<Header title={group.name} arrowColor={route.params?.color ? route.params?.color : colors.Home} titleColor={route.params?.color ? route.params?.color : colors.Home} />
|
<ScrollView
|
||||||
<Text style={styles.description}>{group.description}</Text>
|
refreshControl={
|
||||||
|
<RefreshControl refreshing={refreshing || loading} onRefresh={onRefresh} colors={[colors.black]} />
|
||||||
|
}
|
||||||
|
contentContainerStyle={styles.scrollContent}
|
||||||
|
>
|
||||||
|
<Header title={group.name} />
|
||||||
|
<Text style={styles.description}>{group.description}</Text>
|
||||||
|
|
||||||
{/* Progress Bar */}
|
<View style={styles.statsRow}>
|
||||||
<View style={styles.progressContainer}>
|
<View style={styles.statBox}>
|
||||||
<View style={styles.progressBar}>
|
<Text style={styles.statValue}>{group.members.length ?? 0}</Text>
|
||||||
<View style={{ flex: progress, backgroundColor: route.params?.color ? route.params?.color : colors.Home }} />
|
<Text style={styles.statLabel}>Members</Text>
|
||||||
<View style={{ flex: 1 - progress }} />
|
</View>
|
||||||
|
<View style={styles.statBox}>
|
||||||
|
<Text style={styles.statValue}>₹{group.collected}</Text>
|
||||||
|
<Text style={styles.statLabel}>Contribution</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.statBox}>
|
||||||
|
<Text style={styles.statValue}>₹{group.goal}</Text>
|
||||||
|
<Text style={styles.statLabel}>Total Amount</Text>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.amount}>₹{group.collected} / ₹{group.goal}</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Stats */}
|
{cycleItems.length > 0 && (
|
||||||
<View style={styles.statsRow}>
|
<CustomDropdown
|
||||||
<View style={styles.statBox}>
|
items={cycleItems}
|
||||||
<Text style={styles.statValue}>{group.members.length ?? []}</Text>
|
selectedValue={selectedCycleNumber}
|
||||||
<Text style={styles.statLabel}>Members</Text>
|
onValueChange={setSelectedCycleNumber}
|
||||||
|
placeholder="Select a Cycle"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedCycleData && cycleDashboardData && (
|
||||||
|
<CycleDashboard data={{ ...cycleDashboardData, cycles: [selectedCycleData] }} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<View style={styles.membersHeader}>
|
||||||
|
<Text style={styles.sectionTitle}>Members</Text>
|
||||||
|
<View style={styles.statusBadge}>
|
||||||
|
<Text style={styles.statusText}>{group.status}</Text>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.statBox}>
|
|
||||||
<Text style={styles.statValue}>₹{group.collected}</Text>
|
|
||||||
<Text style={styles.statLabel}>Collected</Text>
|
|
||||||
</View>
|
|
||||||
<View style={styles.statBox}>
|
|
||||||
<Text style={styles.statValue}>₹{group.goal}</Text>
|
|
||||||
<Text style={styles.statLabel}>Goal</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Members List */}
|
{memberData.map((member:any) => (
|
||||||
<View style={styles.statsRow}>
|
<View key={member.userId} style={styles.memberCard}>
|
||||||
<Text style={styles.sectionTitle}>Members</Text>
|
<Ionicons name="person" size={40} color={colors.black} />
|
||||||
<Text style={[styles.sectionTitle, { backgroundColor: route.params?.color ? colors.pastelOrange : colors.pastelPink, color: route.params?.color ? colors.Settings : colors.Home, paddingHorizontal: 5 }]}>{group.status}</Text>
|
<View style={styles.memberInfo}>
|
||||||
</View>
|
<Text style={styles.memberName}>
|
||||||
|
{member.firstName} {member.role === "Organizer" ? "(Organizer)" : ""}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
|
||||||
<FlatList
|
{/* Button at the bottom inside scroll (no absolute positioning) */}
|
||||||
data={group.members}
|
{group.status === 'ACTIVE' && !payable?.hasPaid && (
|
||||||
keyExtractor={(item) => item.id}
|
<TouchableOpacity onPress={() => setOpenModal(true)} style={styles.button}>
|
||||||
renderItem={renderMember}
|
|
||||||
contentContainerStyle={styles.membersListContainer}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Actions */}
|
|
||||||
{
|
|
||||||
group.status === 'ACTIVE' &&
|
|
||||||
|
|
||||||
<View style={styles.actions}>
|
|
||||||
<TouchableOpacity onPress={() => {
|
|
||||||
setOpenModal(true)
|
|
||||||
handleContribution()
|
|
||||||
}}
|
|
||||||
style={[styles.button, { backgroundColor: route.params?.color ? route.params?.color : colors.Home }]}>
|
|
||||||
<Text style={styles.buttonText}>Contribute</Text>
|
<Text style={styles.buttonText}>Contribute</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
)}
|
||||||
}
|
</ScrollView>
|
||||||
{
|
|
||||||
openModal && <ContributionScreen data={payable} visible={openModal} onClose={() => setOpenModal(!openModal)} onConfirm={() => {
|
{openModal && (
|
||||||
confirmPayment()
|
<ContributionScreen
|
||||||
setOpenModal(false)
|
data={payable}
|
||||||
}} />
|
visible={openModal}
|
||||||
}
|
onClose={() => setOpenModal(false)}
|
||||||
|
onConfirm={() => {
|
||||||
|
confirmPayment();
|
||||||
|
setOpenModal(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</AppView>
|
</AppView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default GroupDetails;
|
export default GroupDetails;
|
||||||
|
|
||||||
// Styles remain mostly unchanged
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
|
dropdownWrapper: {
|
||||||
|
marginHorizontal: 15,
|
||||||
|
marginBottom: 20,
|
||||||
|
},
|
||||||
|
dropdownHeader: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: 12,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#ddd',
|
||||||
|
borderRadius: 8,
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
},
|
||||||
|
modalOverlay: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.4)',
|
||||||
|
padding: 20,
|
||||||
|
},
|
||||||
|
dropdownListContainer: {
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
borderRadius: 8,
|
||||||
|
padding: 10,
|
||||||
|
maxHeight: 300,
|
||||||
|
},
|
||||||
|
dropdownItem: {
|
||||||
|
paddingVertical: 12,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
},
|
||||||
|
selectedItem: {
|
||||||
|
backgroundColor: colors.pastelOrange,
|
||||||
|
borderRadius: 6,
|
||||||
|
},
|
||||||
|
itemText: {
|
||||||
|
fontSize: 15,
|
||||||
|
color: colors.black,
|
||||||
|
},
|
||||||
description: {
|
description: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: "#475569",
|
color: "#475569",
|
||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
lineHeight: 20,
|
lineHeight: 20,
|
||||||
|
paddingHorizontal: 15,
|
||||||
},
|
},
|
||||||
progressContainer: { marginBottom: 24 },
|
statsRow: {
|
||||||
progressBar: {
|
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
height: 8,
|
justifyContent: "space-around",
|
||||||
borderRadius: 4,
|
marginBottom: 20,
|
||||||
backgroundColor: "#e2e8f0",
|
paddingHorizontal: 15,
|
||||||
overflow: "hidden",
|
|
||||||
marginBottom: 6,
|
|
||||||
},
|
},
|
||||||
amount: { fontSize: 13, color: "#64748b" },
|
|
||||||
statsRow: { flexDirection: "row", justifyContent: "space-between", marginBottom: 20 },
|
|
||||||
statBox: {
|
statBox: {
|
||||||
|
alignItems: 'center',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: "#fff",
|
},
|
||||||
marginHorizontal: 4,
|
statValue: {
|
||||||
padding: 16,
|
fontSize: 18,
|
||||||
borderRadius: 12,
|
fontWeight: '700',
|
||||||
alignItems: "center",
|
color: '#1e293b',
|
||||||
shadowColor: "#000",
|
},
|
||||||
shadowOpacity: 0.05,
|
statLabel: {
|
||||||
shadowOffset: { width: 0, height: 2 },
|
fontSize: 12,
|
||||||
shadowRadius: 6,
|
color: '#64748b',
|
||||||
|
marginTop: 4,
|
||||||
|
},
|
||||||
|
membersHeader: {
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingHorizontal: 15,
|
||||||
|
marginTop: 20,
|
||||||
|
marginBottom: 10,
|
||||||
|
},
|
||||||
|
sectionTitle: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#0f172a",
|
||||||
|
},
|
||||||
|
statusBadge: {
|
||||||
|
backgroundColor: colors.pastelOrange,
|
||||||
|
borderRadius: 20,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingVertical: 6,
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
},
|
},
|
||||||
statValue: { fontSize: 18, fontWeight: "700", color: "#1e293b" },
|
statusText: {
|
||||||
statLabel: { fontSize: 12, color: "#64748b", marginTop: 4 },
|
fontSize: 14,
|
||||||
sectionTitle: { fontSize: 16, fontWeight: "600", color: "#0f172a", marginBottom: 12 },
|
fontWeight: '600',
|
||||||
membersListContainer: { paddingBottom: 20 },
|
color: colors.Home,
|
||||||
|
},
|
||||||
memberCard: {
|
memberCard: {
|
||||||
backgroundColor: "#fff",
|
flexDirection: "row",
|
||||||
padding: 16,
|
alignItems: 'center',
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
padding: 12,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
marginBottom: 10,
|
marginBottom: 10,
|
||||||
borderLeftWidth: 5,
|
elevation: 2,
|
||||||
|
marginHorizontal: 15,
|
||||||
|
},
|
||||||
|
memberInfo: {
|
||||||
|
marginLeft: 10,
|
||||||
|
},
|
||||||
|
memberName: {
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#1e293b",
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
backgroundColor: colors.pastelOrange,
|
||||||
|
marginHorizontal: 15,
|
||||||
|
marginTop: 20,
|
||||||
|
marginBottom: 30,
|
||||||
|
elevation: 5,
|
||||||
|
paddingVertical: 14,
|
||||||
|
borderRadius: 10,
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
buttonText: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: "#000",
|
||||||
|
},
|
||||||
|
scrollContent: {
|
||||||
|
paddingBottom: 50,
|
||||||
},
|
},
|
||||||
memberName: { fontSize: 15, fontWeight: "600", color: "#1e293b", marginBottom: 4 },
|
|
||||||
memberDetail: { fontSize: 13, color: "#475569" },
|
|
||||||
actions: { marginTop: 16 },
|
|
||||||
button: { backgroundColor: colors.Home, paddingVertical: 14, borderRadius: 10, alignItems: "center", marginBottom: 12 },
|
|
||||||
buttonText: { fontSize: 15, fontWeight: "600", color: "#fff" },
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -127,7 +127,8 @@ const ManagementScreen: FC = () => {
|
|||||||
case "My Groups":
|
case "My Groups":
|
||||||
return <FlatList data={myGroups} renderItem={({ item }) => <MyGroupCard item={item} onManage={handleManageGroup} />} keyExtractor={(item) => item.id} ListEmptyComponent={<Text style={styles.emptyText}>You haven't created any groups yet.</Text>} onRefresh={fetchData} refreshing={loading} contentContainerStyle={{ paddingBottom: 100 }} />;
|
return <FlatList data={myGroups} renderItem={({ item }) => <MyGroupCard item={item} onManage={handleManageGroup} />} keyExtractor={(item) => item.id} ListEmptyComponent={<Text style={styles.emptyText}>You haven't created any groups yet.</Text>} onRefresh={fetchData} refreshing={loading} contentContainerStyle={{ paddingBottom: 100 }} />;
|
||||||
case "Joined Groups":
|
case "Joined Groups":
|
||||||
return <FlatList data={joinedGroups} renderItem={({ item }) => <JoinedGroupCard item={item} onViewPress={() => handleViewGroup(item)} />} keyExtractor={(item) => item.id} ListEmptyComponent={<Text style={styles.emptyText}>You haven't joined any groups yet.</Text>} onRefresh={fetchData} refreshing={loading} contentContainerStyle={{ paddingBottom: 100 }} />;
|
return <FlatList data={joinedGroups} renderItem={({ item }) => <JoinedGroupCard item={item} onViewPress={() => {
|
||||||
|
handleViewGroup(item)}} />} keyExtractor={(item) => item.id} ListEmptyComponent={<Text style={styles.emptyText}>You haven't joined any groups yet.</Text>} onRefresh={fetchData} refreshing={loading} contentContainerStyle={{ paddingBottom: 100 }} />;
|
||||||
case "Requests":
|
case "Requests":
|
||||||
return <FlatList data={requests} renderItem={({ item }) => <RequestCard item={item} onApprove={handleApprove} onDecline={handleDecline} />} keyExtractor={(item) => item.requestId} ListEmptyComponent={<Text style={styles.emptyText}>No pending join requests.</Text>} onRefresh={fetchData} refreshing={loading} contentContainerStyle={{ paddingBottom: 100 }} />;
|
return <FlatList data={requests} renderItem={({ item }) => <RequestCard item={item} onApprove={handleApprove} onDecline={handleDecline} />} keyExtractor={(item) => item.requestId} ListEmptyComponent={<Text style={styles.emptyText}>No pending join requests.</Text>} onRefresh={fetchData} refreshing={loading} contentContainerStyle={{ paddingBottom: 100 }} />;
|
||||||
case "Your Requests":
|
case "Your Requests":
|
||||||
|
|||||||
Reference in New Issue
Block a user