diff --git a/package-lock.json b/package-lock.json index 8bb0676..ef44305 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4858,6 +4858,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -8223,6 +8224,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "devOptional": true, "license": "ISC" }, "node_modules/istanbul-lib-coverage": { @@ -8973,6 +8975,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "devOptional": true, "license": "MIT" }, "node_modules/json-schema-traverse": { @@ -9856,6 +9859,7 @@ "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -13885,7 +13889,6 @@ "version": "6.4.0", "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", - "devOptional": true, "license": "MIT", "dependencies": { "regenerate": "^1.4.2", @@ -13909,7 +13912,6 @@ "version": "0.13.0", "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz", "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==", - "devOptional": true, "license": "BSD-2-Clause", "dependencies": { "jsesc": "~3.1.0" @@ -13918,18 +13920,6 @@ "regjsparser": "bin/parser" } }, - "node_modules/regjsparser/node_modules/jsesc": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", - "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -14951,6 +14941,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, "license": "MIT" }, "node_modules/throat": { @@ -15405,6 +15396,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "devOptional": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index 21a26dd..1b460d5 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -41,6 +41,7 @@ export type RootStackParamList = { Invite: { groupId: string | number; code: string | number }; ManagementTabs:{ groupId: string | number;} TransactionList:undefined + RandomSelectUserScreen: { groupId: string | number, cycleNumber: number }; }; const Stack = createNativeStackNavigator(); @@ -93,7 +94,7 @@ const linking = { - + ); diff --git a/src/screens/home/Home.tsx b/src/screens/home/Home.tsx index 8c55098..25cef32 100644 --- a/src/screens/home/Home.tsx +++ b/src/screens/home/Home.tsx @@ -222,7 +222,7 @@ const Home: React.FC = () => { return ( - Hello, {user.firstName || apiUser?.firstName} {user.lastName || apiUser?.lastName}👋 + Hello, {user.firstName || apiUser?.firstName} {user.lastName || apiUser?.lastName} navigation.navigate("Profile" as never)}> { {/* Welcome Back 👋 */} ; // --- Main Screen Component --- const EditGroupScreen: React.FC = ({ route }) => { const { groupId } = route.params; - + const navigation = useNavigation(); const [editingCard, setEditingCard] = useState(null); const [groupDetails, setGroupDetails] = useState(null); @@ -105,7 +106,8 @@ const EditGroupScreen: React.FC = ({ route }) => { const [starting, setStarting] = useState(false); const [inviting, setInviting] = useState(false); const [error, setError] = useState(null); - + const [paymentStatus, setPaymentStatus] = useState(null); + const [currentCycle, setCurrentCycle] = useState(null); const initializeEditableData = (data: GroupDetails) => { setEditableData({ @@ -141,10 +143,19 @@ const EditGroupScreen: React.FC = ({ 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]); @@ -177,7 +188,9 @@ const EditGroupScreen: React.FC = ({ route }) => { setSaving(false); } }; - + const handleDistribute = async () => { + navigation.navigate('RandomSelectUserScreen', { groupId: groupDetails?.id || '', cycleNumber: currentCycle || 0 }); + }; const handleStartGroup = async () => { if (!groupDetails) return; @@ -322,7 +335,6 @@ const EditGroupScreen: React.FC = ({ route }) => { ); } - if (loading) return ; if (error) return {error}; if (!groupDetails) return null; @@ -332,7 +344,6 @@ const EditGroupScreen: React.FC = ({ route }) => { const isInvitable = groupDetails.status === 'APPROVED'; const canStartGroup = groupDetails.status === 'APPROVED' && groupDetails.totalParticipants >= groupDetails.minParticipants; - return ( @@ -414,6 +425,21 @@ const EditGroupScreen: React.FC = ({ route }) => { )} + {paymentStatus && ( + + + {starting ? ( + + ) : ( + Distribute + )} + + + )} ); }; diff --git a/src/screens/management/RandomSelectUserScreen.tsx b/src/screens/management/RandomSelectUserScreen.tsx index 36a8c09..531f031 100644 --- a/src/screens/management/RandomSelectUserScreen.tsx +++ b/src/screens/management/RandomSelectUserScreen.tsx @@ -1,248 +1,285 @@ import React, { useState, useEffect, useRef } from 'react'; -import { View, Text, StyleSheet, TouchableOpacity, Animated, Easing, SafeAreaView, Dimensions, Alert } from 'react-native'; -import { Confetti, ConfettiMethods } from 'react-native-fast-confetti'; +import { View, Text, StyleSheet, TouchableOpacity, FlatList, Dimensions, Alert, ActivityIndicator, Animated } from 'react-native'; +import { apiCall } from '../../utils/ApiHelper'; +import { Ionicons } from "@react-native-vector-icons/ionicons"; -import AppView from '../../components/AppView'; - - -// --- PROPS & TYPE DEFINITIONS --- -export interface RandomWinnerScreenProps { - participants: Participant[]; - excludedParticipantIds: string[]; -} - -export interface Participant { - id: string; - name: string; -} - -// --- CONSTANTS --- const { width } = Dimensions.get('window'); -const ITEM_HEIGHT = 70; // Height of each name item -const ANIMATION_DURATION = 8000; // 8 seconds for suspense -const SHUFFLE_MULTIPLIER = 5; // Repeats the list for a longer scroll effect +const NUM_COLUMNS = 3; +const ITEM_SIZE = width / NUM_COLUMNS - 20; -const RandomSelectUserScreen: React.FC = () => { - // --- MOCK DATA (Replace with navigation props) --- - const mockProps: RandomWinnerScreenProps = { - participants: [ - { id: '1', name: 'Vinay M.' }, - { id: '2', name: 'Nitesh Y.' }, - { id: '3', name: 'Alice' }, - { id: '4', name: 'Bob' }, - { id: '5', name: 'Charlie' }, - { id: '6', name: 'David' }, - { id: '7', name: 'Eve' }, - ], - excludedParticipantIds: ['4'], // Exclude Bob - }; - // --- END MOCK DATA --- +const RandomSelectUserScreen: React.FC<{ route: any }> = ({ route }) => { + const { groupId, cycleNumber } = route.params; + const [users, setUsers] = useState<{ id: string; name: string }[]>([]); + const [highlightedIndex, setHighlightedIndex] = useState(null); + const [winner, setWinner] = useState<{ id: string; name: string } | null>(null); + const [isAnimating, setIsAnimating] = useState(false); + const [isLoading, setIsLoading] = useState(true); - const { participants, excludedParticipantIds } = mockProps; + const scaleAnim = useRef(new Animated.Value(1)).current; - const [eligibleParticipants, setEligibleParticipants] = useState([]); - const [isSelecting, setIsSelecting] = useState(false); - const [winner, setWinner] = useState(null); - const [animationList, setAnimationList] = useState([]); - - const scrollY = useRef(new Animated.Value(0)).current; - const confettiRef = useRef(null); - - // Filter participants useEffect(() => { - const eligible = participants.filter(p => !excludedParticipantIds.includes(p.id)); - setEligibleParticipants(eligible); - }, [participants, excludedParticipantIds]); + const fetchParticipants = async () => { + try { + const response = await apiCall(`https://exotic-humpback-mistakenly.ngrok-free.app/api/fund/group/all/member?groupId=${groupId}`, "GET") as any; + if (response.status === 'success' && response.data) { + const formattedUsers = response.data.map((u: any) => ({ + id: u.id, + name: `${u.firstName} ${u.lastName}`, + })); + setUsers(formattedUsers); + } else { + Alert.alert('Error', 'Failed to fetch participants.'); + } + } catch (error) { + Alert.alert('Error', 'Could not connect to the server to fetch participants.'); + } finally { + setIsLoading(false); + } + }; + + fetchParticipants(); + }, [groupId]); + + useEffect(() => { + if (highlightedIndex !== null) { + Animated.sequence([ + Animated.timing(scaleAnim, { toValue: 1.15, duration: 80, useNativeDriver: true }), + Animated.timing(scaleAnim, { toValue: 1, duration: 80, useNativeDriver: true }), + ]).start(); + } + }, [highlightedIndex]); const handleStartSelection = () => { - if (eligibleParticipants.length < 2) { - Alert.alert('Not enough eligible participants.'); - return; - } + if (users.length === 0 || isAnimating) return; - setIsSelecting(true); setWinner(null); - scrollY.setValue(0); + setIsAnimating(true); - // 1. Secretly select winner - const winnerIndex = Math.floor(Math.random() * eligibleParticipants.length); - const selectedWinner = eligibleParticipants[winnerIndex]; + let highlightCount = 0; + const maxHighlights = 30 + Math.floor(Math.random() * 10); // Add some randomness - // 2. Create long, shuffled list for animation - let listForAnimation: Participant[] = []; - for (let i = 0; i < SHUFFLE_MULTIPLIER; i++) { - const shuffled = [...eligibleParticipants].sort(() => Math.random() - 0.5); - listForAnimation.push(...shuffled); - } - - // 3. Place winner at the end - listForAnimation.push(selectedWinner); - setAnimationList(listForAnimation); + const highlightInterval = setInterval(() => { + const randomIndex = Math.floor(Math.random() * users.length); + setHighlightedIndex(randomIndex); + highlightCount++; - // 4. Calculate final scroll position - const finalY = -(listForAnimation.length - 1) * ITEM_HEIGHT; - - // 5. Start animation - Animated.timing(scrollY, { - toValue: finalY, - duration: ANIMATION_DURATION, - useNativeDriver: true, - easing: Easing.bezier(0.3, 0, 0.1, 1), - }).start(() => { - setWinner(selectedWinner); - confettiRef.current?.start(); - }); + if (highlightCount >= maxHighlights) { + clearInterval(highlightInterval); + fetchWinner(); + } + }, 120); }; - const handleReset = () => { - setIsSelecting(false); - setWinner(null); - scrollY.setValue(0); - confettiRef.current?.stop(); + const fetchWinner = async () => { + const payload = { groupId: groupId }; + try { + const response = await apiCall('https://exotic-humpback-mistakenly.ngrok-free.app/api/fund/method/based/userSelect', 'POST', payload) as any; + + if (response.status === 'success' && response.data?.id) { + const winnerData = { id: response.data.id, name: `${response.data.firstName} ${response.data.lastName}` }; + const winnerIndex = users.findIndex(u => u.id === winnerData.id); + + if (winnerIndex !== -1) { + setWinner(winnerData); + setHighlightedIndex(winnerIndex); + } else { + Alert.alert('Error', 'Winner not found among participants.'); + } + } else { + Alert.alert('Error', 'Failed to determine a winner from the API.'); + } + } catch (error) { + Alert.alert('Error', 'Could not connect to the server to fetch the winner.'); + } finally { + setIsAnimating(false); + } }; - const renderListItem = ({ item, index }: { item: Participant, index: number }) => ( - - {item.name} - - ); + const renderItem = ({ item, index }: { item: { id: string; name: string }, index: number }) => { + const isHighlighted = index === highlightedIndex; + const isWinner = winner?.id === item.id && !isAnimating; + + const animatedStyle = { + transform: [{ scale: isHighlighted ? scaleAnim : 1 }], + }; + + return ( + + + + {item.name} + + + ); + }; + + if (isLoading) { + return ( + + + Loading Participants... + + ); + } return ( - - - - Select a Winner - - - - {isSelecting || winner ? ( - - {animationList.map((item, index) => renderListItem({ item, index }))} - - ) : ( - - Press Start to Select + + + Select a Winner + Cycle #{cycleNumber} + + item.id} + numColumns={NUM_COLUMNS} + extraData={{ highlightedIndex, winner }} + contentContainerStyle={styles.grid} + /> + + {winner && !isAnimating && ( + + 🎉 Winner: {winner.name} )} + + {isAnimating ? 'Selecting...' : 'Start Selection'} + - - {winner && ( - - Winner is - {winner.name}! - - )} - - {!isSelecting && !winner && ( - - Start Selection - - )} - - {winner && ( - - Select Again - - )} - - + ); }; const styles = StyleSheet.create({ + centeredContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: '#f9fafb', + }, + loadingText: { + marginTop: 15, + fontSize: 18, + color: '#374151', + fontWeight: '500', + }, container: { flex: 1, - backgroundColor: '#1C1C1E', + backgroundColor: '#f9fafb', alignItems: 'center', - justifyContent: 'space-between', - paddingVertical: 40, + }, + header: { + paddingTop: 50, + paddingBottom: 20, + alignItems: 'center' }, title: { fontSize: 32, fontWeight: 'bold', - color: '#FFFFFF', + color: '#111827', + }, + subtitle: { + fontSize: 18, + color: '#6b7280', + marginTop: 4 + }, + grid: { + paddingHorizontal: 10, + }, + gridItem: { + margin: 8, + width: ITEM_SIZE, + height: ITEM_SIZE, + justifyContent: 'center', + alignItems: 'center', + borderRadius: 12, + backgroundColor: '#fff', + borderWidth: 1, + borderColor: '#e5e7eb', + padding: 8, + shadowColor: "#000", + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.1, + shadowRadius: 3.84, + elevation: 5, + }, + highlightedItem: { + borderWidth: 5, + borderColor: '#4f46e5', + }, + winnerItem: { + backgroundColor: '#10b981', + borderColor: '#059669', + }, + gridText: { + marginTop: 8, + color: '#1f2937', + fontWeight: '600', textAlign: 'center', - marginBottom: 30, + fontSize: 12, }, - selectionMachine: { - height: ITEM_HEIGHT, - width: '90%', - backgroundColor: '#2C2C2E', - borderRadius: 20, - overflow: 'hidden', - justifyContent: 'center', + highlightedText: { + color: '#000000ff', }, - winnerPointer: { - position: 'absolute', - width: '105%', - height: ITEM_HEIGHT, - borderRadius: 20, - borderWidth: 3, - borderColor: '#FBC02D', - alignSelf: 'center', - zIndex: 1, + footer: { + width: '100%', + padding: 20, + borderTopWidth: 1, + borderTopColor: '#e5e7eb', + alignItems: 'center', + backgroundColor: '#fff' }, - itemContainer: { - height: ITEM_HEIGHT, - justifyContent: 'center', - alignItems: 'center', + button: { + paddingVertical: 16, + paddingHorizontal: 50, + backgroundColor: '#4f46e5', + borderRadius: 30, + elevation: 3, + shadowColor: "#4f46e5", + shadowOffset: { + width: 0, + height: 4, + }, + shadowOpacity: 0.3, + shadowRadius: 4.65, }, - itemName: { - fontSize: 36, + buttonDisabled: { + backgroundColor: '#9ca3af', + shadowColor: 'transparent', + }, + buttonText: { + fontSize: 18, + color: '#fff', fontWeight: 'bold', - color: '#FFF', }, - placeholderContainer: { - height: ITEM_HEIGHT, - justifyContent: 'center', - alignItems: 'center', - }, - placeholderText: { - fontSize: 24, - color: '#666', - fontStyle: 'italic', - }, - winnerBanner: { - marginTop: 30, - padding: 20, - backgroundColor: '#FBC02D', - borderRadius: 15, + winnerTextContainer: { + marginBottom: 20, + padding: 12, + backgroundColor: '#d1fae5', + borderRadius: 12, + width: '100%', alignItems: 'center', }, winnerText: { fontSize: 20, - color: '#000', - }, - winnerName: { - fontSize: 40, - fontWeight: 'bold', - color: '#000', - }, - startButton: { - backgroundColor: '#007AFF', - paddingVertical: 18, - paddingHorizontal: 40, - borderRadius: 30, - marginTop: 40, - }, - startButtonText: { - color: '#FFFFFF', - fontSize: 20, - fontWeight: 'bold', - }, - resetButton: { - backgroundColor: '#333', - paddingVertical: 18, - paddingHorizontal: 40, - borderRadius: 30, - marginTop: 40, - }, - resetButtonText: { - color: '#FFFFFF', - fontSize: 20, fontWeight: 'bold', + color: '#065f46', }, }); export default RandomSelectUserScreen; - diff --git a/src/utils/ApiHelper.tsx b/src/utils/ApiHelper.tsx index 2da263b..3306406 100644 --- a/src/utils/ApiHelper.tsx +++ b/src/utils/ApiHelper.tsx @@ -36,10 +36,10 @@ export async function apiCall( }); // --- Response Logging --- - console.log(`--- API Response: [${response.status}] ${url} ---`); + console.log(`--- API Response: [${response}] ${url} ---`); // Uncomment the line below to see the full response data in logs // console.log("Response Data:", JSON.stringify(response.data, null, 2)); - console.log("---------------------------------------"); + console.log("---------------------------------------",response); return response.data; } catch (error: any) {