enovate new dashboard lik a real time application

This commit is contained in:
Jaanu423
2025-10-14 11:27:27 +05:30
20 changed files with 540 additions and 326 deletions
Binary file not shown.
+37
View File
@@ -0,0 +1,37 @@
{
"version": 3,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "com.circlecollect",
"variantName": "release",
"elements": [
{
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 1,
"versionName": "1.0",
"outputFile": "app-release.apk"
}
],
"elementType": "File",
"baselineProfiles": [
{
"minApi": 28,
"maxApi": 30,
"baselineProfiles": [
"baselineProfiles/1/app-release.dm"
]
},
{
"minApi": 31,
"maxApi": 2147483647,
"baselineProfiles": [
"baselineProfiles/0/app-release.dm"
]
}
],
"minSdkVersionForDexing": 24
}
Binary file not shown.
+14
View File
@@ -38,6 +38,7 @@
"react-native-inset-shadow": "^1.0.3", "react-native-inset-shadow": "^1.0.3",
"react-native-keychain": "^10.0.0", "react-native-keychain": "^10.0.0",
"react-native-linear-gradient": "^2.8.3", "react-native-linear-gradient": "^2.8.3",
"react-native-material-ripple": "^0.9.1",
"react-native-paper": "^5.14.5", "react-native-paper": "^5.14.5",
"react-native-reanimated": "^4.1.2", "react-native-reanimated": "^4.1.2",
"react-native-safe-area-context": "^5.6.1", "react-native-safe-area-context": "^5.6.1",
@@ -12932,6 +12933,19 @@
"react-native": "*" "react-native": "*"
} }
}, },
"node_modules/react-native-material-ripple": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/react-native-material-ripple/-/react-native-material-ripple-0.9.1.tgz",
"integrity": "sha512-dHjARdKoGD3LXwPh2TkURabywM5BfdpKc/AHplJTfBMalhVZ2bc7iJoJmNhNnmT/TZ+B3szL6uV1mO7Pv8zNeA==",
"license": "BSD-3-Clause",
"dependencies": {
"prop-types": "^15.5.10"
},
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/react-native-paper": { "node_modules/react-native-paper": {
"version": "5.14.5", "version": "5.14.5",
"license": "MIT", "license": "MIT",
+1
View File
@@ -40,6 +40,7 @@
"react-native-inset-shadow": "^1.0.3", "react-native-inset-shadow": "^1.0.3",
"react-native-keychain": "^10.0.0", "react-native-keychain": "^10.0.0",
"react-native-linear-gradient": "^2.8.3", "react-native-linear-gradient": "^2.8.3",
"react-native-material-ripple": "^0.9.1",
"react-native-paper": "^5.14.5", "react-native-paper": "^5.14.5",
"react-native-reanimated": "^4.1.2", "react-native-reanimated": "^4.1.2",
"react-native-safe-area-context": "^5.6.1", "react-native-safe-area-context": "^5.6.1",
+3 -3
View File
@@ -33,12 +33,12 @@ const Header: React.FC<HeaderProps> = ({
return ( return (
<View style={styles.header}> <View style={styles.header}>
{showBack && ( {/* {showBack && (
<TouchableOpacity onPress={handleBack} style={{padding:6}}> <TouchableOpacity onPress={handleBack} style={{padding:6}}>
<Ionicons name="arrow-back" size={24} color={arrowColor} /> <Ionicons name="arrow-back" size={24} color={arrowColor} />
</TouchableOpacity> </TouchableOpacity>
)} )} */}
<Text style={[styles.headerTitle, {color:titleColor}]}>{title}</Text> <Text style={[styles.headerTitle, {color:colors.black}]}>{title}</Text>
<View>{rightAction}</View> <View>{rightAction}</View>
</View> </View>
); );
+188 -162
View File
@@ -1,6 +1,4 @@
// src/screens/LoginScreen.tsx import React, { useState, useRef, useEffect } from 'react';
import React, { useState, useRef, useEffect } from "react";
import { import {
View, View,
Text, Text,
@@ -12,27 +10,34 @@ import {
Animated, Animated,
StatusBar, StatusBar,
ActivityIndicator, ActivityIndicator,
} from "react-native"; Dimensions,
import { LOGIN_URL } from "../utils/ApiUrls"; } from 'react-native';
import axios from "axios"; import { LOGIN_URL } from '../utils/ApiUrls';
import { useNavigation } from "@react-navigation/native"; import axios from 'axios';
import type { StackNavigationProp } from "@react-navigation/stack"; import { useNavigation } from '@react-navigation/native';
import { RootStackParamList } from "../navigation/AppNavigator"; import type { StackNavigationProp } from '@react-navigation/stack';
import LinearGradient from "react-native-linear-gradient"; import { RootStackParamList } from '../navigation/AppNavigator';
import LinearGradient from 'react-native-linear-gradient';
import Ionicons from '@react-native-vector-icons/ionicons'; import Ionicons from '@react-native-vector-icons/ionicons';
import CustomAlert from "../components/CostomAlart"; import MaterialRipple from 'react-native-material-ripple'; // For the button ripple effect
import colors from "../components/colors"; // Centralized colors from your file import CustomAlert from '../components/CostomAlart';
import colors from '../components/colors';
const { width } = Dimensions.get('window');
const BLOCK_SIZE = width / 3;
// Note: You will need to install react-native-material-ripple
// npm install react-native-material-ripple
export default function LoginScreen() { export default function LoginScreen() {
const navigation = useNavigation<StackNavigationProp<RootStackParamList>>(); const navigation = useNavigation<StackNavigationProp<RootStackParamList>>();
const [mobile, setMobile] = useState(""); const [mobile, setMobile] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [alertInfo, setAlertInfo] = useState({ const [alertInfo, setAlertInfo] = useState({
visible: false, visible: false,
title: "", title: '',
message: "", message: '',
buttons: [] as { text: string; onPress: () => void; style?: "cancel" | "destructive" | "default" }[], buttons: [] as { text: string; onPress: () => void; style?: 'cancel' | 'destructive' | 'default' }[],
alertType: 'info' as 'info' | 'success' | 'error', alertType: 'info' as 'info' | 'success' | 'error',
}); });
@@ -50,12 +55,12 @@ export default function LoginScreen() {
const showAlert = ( const showAlert = (
title: string, title: string,
message: string, message: string,
buttons: { text: string; onPress: () => void; style?: "cancel" | "destructive" | "default" }[], buttons: { text: string; onPress: () => void; style?: 'cancel' | 'destructive' | 'default' }[],
alertType: 'info' | 'success' | 'error' = 'info' alertType: 'info' | 'success' | 'error' = 'info',
) => { ) => {
setAlertInfo({ visible: true, title, message, buttons, alertType }); setAlertInfo({ visible: true, title, message, buttons, alertType });
}; };
const hideAlert = () => { const hideAlert = () => {
setAlertInfo({ ...alertInfo, visible: false }); setAlertInfo({ ...alertInfo, visible: false });
}; };
@@ -64,68 +69,70 @@ export default function LoginScreen() {
if (mobile.length === 10) { if (mobile.length === 10) {
setLoading(true); setLoading(true);
try { try {
console.log("Login api call:");
// FIX: Used the imported LOGIN_URL constant instead of a string literal
const res = await axios.get(LOGIN_URL, { params: { mobile } }); const res = await axios.get(LOGIN_URL, { params: { mobile } });
console.log("Login response:", res.data);
if (res.data.statusCode === 200) { if (res.data.statusCode === 200) {
navigation.navigate("OtpVarify", { mobile }); navigation.navigate('OtpVarify', { mobile });
} else { } else {
showAlert( showAlert('Login Failed', res.data.message || 'Could not send OTP.', [{ text: 'Try Again', onPress: hideAlert }], 'error');
"Login Failed",
res.data.message || "Could not send OTP.",
[{ text: "Try Again", onPress: hideAlert }],
'error'
);
} }
} catch (error: any) { } catch (error: any) {
console.error("Login error:", error); showAlert('An Error Occurred', 'Failed to send OTP. Please check your connection and try again.', [{ text: 'OK', onPress: hideAlert }], 'error');
showAlert(
"An Error Occurred",
"Failed to send OTP. Please check your connection and try again.",
[{ text: "OK", onPress: hideAlert }],
'error'
);
} finally { } finally {
setLoading(false); setLoading(false);
} }
} else { } else {
showAlert( showAlert('Invalid Number', 'Please enter a valid 10-digit mobile number.', [{ text: 'Got it', onPress: hideAlert }], 'info');
"Invalid Number",
"Please enter a valid 10-digit mobile number.",
[{ text: "Got it", onPress: hideAlert }],
'info'
);
} }
}; };
const colorsBlocks = [
colors.pastelPink,
colors.pastelBlue,
colors.pastelGreen,
colors.pastelOrange,
colors.pastelBlue,
colors.pastelPink,
colors.pastelGreen,
colors.pastelOrange,
colors.pastelPink,
];
return ( return (
<LinearGradient colors={[colors.pastelBlue, colors.pastelGreen, colors.pastelOrange]} style={styles.gradientContainer}> <View style={styles.container}>
<StatusBar barStyle="dark-content" backgroundColor={colors.pastelOrange} />
{/* Funky grid blocks background (Unchanged) */}
<View style={styles.gridContainer}>
{colorsBlocks.map((bgColor, index) => (
<View
key={index}
style={[
styles.gridBlock,
{ backgroundColor: bgColor },
index % 2 === 0 ? styles.elevatedBlock : {},
]}
/>
))}
</View>
<KeyboardAvoidingView <KeyboardAvoidingView
style={styles.container} style={styles.keyboardAvoidingContainer}
behavior={Platform.OS === "ios" ? "padding" : "height"} behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
> >
<StatusBar barStyle="dark-content" /> <View style={styles.contentContainer}>
<View style={styles.topSection}>
<View style={[styles.shape, styles.shape1]} /> <Animated.View style={{ opacity: fade, transform: [{ translateY: slide }] }}>
<View style={[styles.shape, styles.shape2]} /> <Text style={styles.brand}>CircleCollect</Text>
<View style={[styles.shape, styles.shape3]} /> <Text style={styles.title}>Welcome Back!</Text>
<Text style={styles.subtitle}>
Let's get you signed in to your community.
</Text>
</Animated.View>
</View>
<View style={styles.topSection}>
<Animated.View style={{ opacity: fade, transform: [{ translateY: slide }] }}> <Animated.View style={{ opacity: fade, transform: [{ translateY: slide }] }}>
<Text style={styles.brand}>CircleCollect</Text> <View style={styles.inputWrapper}>
<Text style={styles.title}>Welcome!</Text> <Ionicons name="call-outline" size={24} color={colors.textLight} style={styles.inputIcon} />
<Text style={styles.subtitle}>
Glad to see you again. Let's get you signed in to your community.
</Text>
</Animated.View>
</View>
<View style={styles.formSection}>
<Animated.View style={{ opacity: fade, transform: [{ translateY: slide }] }}>
<View style={styles.inputContainer}>
<Ionicons name="call-outline" size={22} color={colors.textLight} style={styles.inputIcon} />
<TextInput <TextInput
style={styles.input} style={styles.input}
placeholder="Enter your mobile number" placeholder="Enter your mobile number"
@@ -134,24 +141,41 @@ export default function LoginScreen() {
onChangeText={setMobile} onChangeText={setMobile}
keyboardType="phone-pad" keyboardType="phone-pad"
maxLength={10} maxLength={10}
selectionColor={colors.textLight}
/> />
</View> </View>
</Animated.View> </Animated.View>
<Animated.View style={[styles.bottomSection, { transform: [{ scale: ctaScale }] }]}>
<MaterialRipple
onPress={handleContinue}
disabled={loading}
rippleColor={colors.white}
style={styles.button}
rippleCentered
rippleContainerBorderRadius={30}
>
<LinearGradient
colors={[colors.pastelPink, colors.pastelOrange]}
style={styles.buttonGradient}
start={{x: 0, y: 0}}
end={{x: 1, y: 1}}
>
{loading ? (
<ActivityIndicator color={colors.white} />
) : (
<View style={styles.buttonContent}>
<Text style={styles.buttonText}>Continue</Text>
<Ionicons name="arrow-forward-circle-outline" size={22} color={colors.white} style={{marginLeft: 8}} />
</View>
)}
</LinearGradient>
</MaterialRipple>
<Text style={styles.footerText}>
Your privacy is our priority. Your information is always secure.
</Text>
</Animated.View>
</View> </View>
<Animated.View style={[styles.bottomSection, { transform: [{ scale: ctaScale }] }]}>
<TouchableOpacity onPress={handleContinue} disabled={loading} activeOpacity={0.8} style={styles.button}>
{loading ? (
<ActivityIndicator color={colors.white} />
) : (
<Text style={styles.buttonText}>Continue with Mobile</Text>
)}
</TouchableOpacity>
<Text style={styles.footerText}>
Your privacy is our priority. Your information is always secure.
</Text>
</Animated.View>
</KeyboardAvoidingView> </KeyboardAvoidingView>
<CustomAlert <CustomAlert
@@ -161,127 +185,129 @@ export default function LoginScreen() {
buttons={alertInfo.buttons} buttons={alertInfo.buttons}
alertType={alertInfo.alertType} alertType={alertInfo.alertType}
/> />
</LinearGradient> </View>
); );
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
gradientContainer: {
flex: 1,
},
container: { container: {
flex: 1, flex: 1,
},
gridContainer: {
...StyleSheet.absoluteFillObject,
flexDirection: 'row',
flexWrap: 'wrap',
},
gridBlock: {
width: BLOCK_SIZE,
height: BLOCK_SIZE,
margin: 2,
borderRadius: 16,
},
elevatedBlock: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 5 },
shadowOpacity: 0.15,
shadowRadius: 10,
elevation: 7,
},
keyboardAvoidingContainer: {
flex: 1,
},
contentContainer: {
flex: 1,
justifyContent: 'space-between',
padding: 24, padding: 24,
justifyContent: "space-between",
},
shape: {
position: 'absolute',
borderRadius: 1000,
opacity: 0.6,
},
shape1: {
width: 400,
height: 400,
backgroundColor: colors.pastelPink,
top: -150,
left: -100,
},
shape2: {
width: 300,
height: 300,
backgroundColor: colors.pastelBlue,
bottom: -100,
right: -120,
},
shape3: {
width: 200,
height: 200,
backgroundColor: colors.pastelGreen,
bottom: '40%',
left: -80,
opacity: 0.5
}, },
topSection: { topSection: {
flex: 0.8, flex: 0.8,
justifyContent: "center", justifyContent: 'center',
alignItems: 'center' alignItems: 'center',
},
formSection: {
flex: 1,
justifyContent: 'flex-start'
}, },
brand: { brand: {
fontSize: 24, fontSize: 28,
fontWeight: "600", fontWeight: '700',
color: colors.primary, color: colors.primary,
textAlign: "center", textAlign: 'center',
letterSpacing: 1.5, letterSpacing: 2,
marginBottom: 16, marginBottom: 16,
textShadowColor: 'rgba(0, 0, 0, 0.1)',
textShadowOffset: { width: 1, height: 1 },
textShadowRadius: 2,
}, },
title: { title: {
fontSize: 38, fontSize: 40,
fontWeight: "bold", fontWeight: 'bold',
color: colors.textDark, color: colors.textDark,
textAlign: "center", textAlign: 'center',
marginBottom: 12, marginBottom: 14,
}, },
subtitle: { subtitle: {
fontSize: 16, fontSize: 16,
color: colors.textLight, color: colors.textDark,
textAlign: "center", textAlign: 'center',
lineHeight: 24, lineHeight: 26,
maxWidth: 320, maxWidth: 320,
}, },
inputContainer: { inputWrapper: {
flexDirection: "row", flexDirection: 'row',
alignItems: "center", alignItems: 'center',
backgroundColor: colors.white, backgroundColor: 'rgba(255, 255, 255, 0.85)',
borderRadius: 16, borderRadius: 36,
borderWidth: 1, borderWidth: 2,
borderColor: colors.inputBorder, borderColor: colors.pastelPink,
paddingHorizontal: 16, paddingHorizontal: 20,
marginTop: 32, shadowColor: colors.primary,
shadowColor: colors.black, shadowOffset: { width: 0, height: 6 },
shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.2,
shadowOpacity: 0.05, shadowRadius: 14,
shadowRadius: 10, elevation: 8,
elevation: 3,
}, },
inputIcon: { inputIcon: {
marginRight: 12, marginRight: 16,
}, },
input: { input: {
flex: 1, flex: 1,
fontSize: 18,
fontWeight: '600',
color: colors.textLight,
height: 58, height: 58,
fontSize: 16,
color: colors.textDark,
}, },
bottomSection: { bottomSection: {
paddingBottom: Platform.OS === "ios" ? 20 : 10, alignItems: 'center',
paddingHorizontal: 10 paddingBottom: Platform.OS === 'ios' ? 20 : 10,
}, },
button: { button: {
backgroundColor: colors.primary, width: '100%',
paddingVertical: 18,
borderRadius: 30, borderRadius: 30,
alignItems: "center",
justifyContent: "center",
flexDirection: "row",
shadowColor: colors.primary, shadowColor: colors.primary,
shadowOffset: { width: 0, height: 8 }, shadowOffset: { width: 0, height: 10 },
shadowOpacity: 0.3, shadowOpacity: 0.4,
shadowRadius: 15, shadowRadius: 20,
elevation: 8, elevation: 14,
marginTop: 20,
},
buttonGradient: {
borderRadius: 30,
paddingVertical: 18,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
},
buttonContent: {
flexDirection: 'row',
alignItems: 'center',
}, },
buttonText: { buttonText: {
color: colors.white, color: colors.white,
fontWeight: "700", fontWeight: '800',
fontSize: 16, fontSize: 18,
}, },
footerText: { footerText: {
textAlign: "center", fontSize: 14,
color: colors.textLight, color: 'rgba(0,0,0,0.5)',
fontSize: 12, marginTop: 28,
marginTop: 20, textAlign: 'center',
maxWidth: 300,
}, },
}); });
+141 -80
View File
@@ -1,4 +1,4 @@
import { RouteProp, useNavigation, useRoute } from "@react-navigation/native"; import { CommonActions, RouteProp, useNavigation, useRoute } from "@react-navigation/native";
import axios from "axios"; import axios from "axios";
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import { import {
@@ -13,16 +13,18 @@ import {
TextInputKeyPressEventData, TextInputKeyPressEventData,
KeyboardAvoidingView, KeyboardAvoidingView,
StatusBar, StatusBar,
Dimensions,
} from "react-native"; } from "react-native";
import { RootStackParamList } from "../navigation/AppNavigator"; import { RootStackParamList } from "../navigation/AppNavigator";
import * as Keychain from "react-native-keychain"; import * as Keychain from "react-native-keychain";
import { LOGIN_URL, VARIFY_OTP_URL } from "../utils/ApiUrls"; import { LOGIN_URL, VARIFY_OTP_URL } from "../utils/ApiUrls";
import colors from "../components/colors"; import colors from "../components/colors";
import LinearGradient from "react-native-linear-gradient";
import CustomAlert from "../components/CostomAlart"; import CustomAlert from "../components/CostomAlart";
import { getFcmToken } from "../utils/firebase/firebaseServices"; import { getFcmToken } from "../utils/firebase/firebaseServices";
const CELL_COUNT = 6; const CELL_COUNT = 6;
const { width } = Dimensions.get('window');
const BLOCK_SIZE = width / 3;
type OtpVerifyRouteProp = RouteProp<RootStackParamList, "OtpVarify">; type OtpVerifyRouteProp = RouteProp<RootStackParamList, "OtpVarify">;
interface AlertButton { interface AlertButton {
@@ -53,21 +55,21 @@ export default function OtpVerificationScreen() {
const route = useRoute<OtpVerifyRouteProp>(); const route = useRoute<OtpVerifyRouteProp>();
const { mobile } = route.params; const { mobile } = route.params;
const navigation = useNavigation<any>(); const navigation = useNavigation<any>();
// Refs for auto-closing logic // Refs for auto-closing logic
const successActionRef = useRef<(() => void) | null>(null); const successActionRef = useRef<(() => void) | null>(null);
const successTimerRef = useRef<number | null>(null); const successTimerRef = useRef<number | null>(null);
const fade = useRef(new Animated.Value(0)).current; const fade = useRef(new Animated.Value(0)).current;
const slide = useRef(new Animated.Value(24)).current; const slide = useRef(new Animated.Value(30)).current;
const ctaScale = useRef(new Animated.Value(1)).current; const ctaScale = useRef(new Animated.Value(1)).current;
useEffect(() => { useEffect(() => {
Animated.parallel([ Animated.parallel([
Animated.timing(fade, { toValue: 1, duration: 600, useNativeDriver: true }), Animated.timing(fade, { toValue: 1, duration: 800, useNativeDriver: true }),
Animated.timing(slide, { toValue: 0, duration: 600, useNativeDriver: true }), Animated.spring(slide, { toValue: 0, friction: 5, tension: 40, useNativeDriver: true }),
]).start(); ]).start();
}, [fade, slide]); }, []);
useEffect(() => { useEffect(() => {
if (timer <= 0) return; if (timer <= 0) return;
@@ -77,8 +79,8 @@ export default function OtpVerificationScreen() {
const hideAlert = () => { const hideAlert = () => {
if (successTimerRef.current) { if (successTimerRef.current) {
clearTimeout(successTimerRef.current); clearTimeout(successTimerRef.current);
successTimerRef.current = null; successTimerRef.current = null;
} }
setAlertConfig(prev => ({ ...prev, visible: false })); setAlertConfig(prev => ({ ...prev, visible: false }));
}; };
@@ -91,12 +93,12 @@ export default function OtpVerificationScreen() {
autoCloseAction?: () => void autoCloseAction?: () => void
) => { ) => {
setAlertConfig({ visible: true, title, message, buttons, alertType }); setAlertConfig({ visible: true, title, message, buttons, alertType });
if (alertType === 'success' && autoCloseAction) { if (alertType === 'success' && autoCloseAction) {
successActionRef.current = autoCloseAction; successActionRef.current = autoCloseAction;
successTimerRef.current = setTimeout(() => { successTimerRef.current = setTimeout(() => {
successActionRef.current?.(); successActionRef.current?.();
}, 2000); // Auto-close after 2 seconds }, 2000);
} }
}; };
@@ -130,14 +132,18 @@ export default function OtpVerificationScreen() {
await saveToken(jwt); await saveToken(jwt);
await getFcmToken(res.data?.data?.userId); await getFcmToken(res.data?.data?.userId);
let hasActionRun = false; let hasActionRun = false;
const successAction = () => { const successAction = () => {
if (hasActionRun) return; if (hasActionRun) return;
hasActionRun = true; hasActionRun = true;
hideAlert(); hideAlert();
navigation.replace("Home"); navigation.dispatch(
}; CommonActions.reset({
index: 0,
routes: [{ name: 'Home' }],
})
);
};
triggerAlert( triggerAlert(
'Success!', 'Success!',
@@ -162,20 +168,18 @@ export default function OtpVerificationScreen() {
try { try {
setIsResending(true); setIsResending(true);
setTimer(30); setTimer(30);
await axios.get(`${LOGIN_URL}?mobile=${mobile}`, { await axios.get(LOGIN_URL, { params: { mobile } })
headers: { "Content-Type": "application/json", Accept: "*/*" },
});
setCode(Array(CELL_COUNT).fill("")); setCode(Array(CELL_COUNT).fill(""));
inputs.current[0]?.focus(); inputs.current[0]?.focus();
let hasActionRun = false; let hasActionRun = false;
const successAction = () => { const successAction = () => {
if (hasActionRun) return; if (hasActionRun) return;
hasActionRun = true; hasActionRun = true;
hideAlert(); hideAlert();
}; };
triggerAlert( triggerAlert(
'OTP Sent', 'OTP Sent',
'A new code has been sent to your mobile number.', 'A new code has been sent to your mobile number.',
@@ -233,73 +237,130 @@ export default function OtpVerificationScreen() {
inputs.current[idx - 1]?.focus(); inputs.current[idx - 1]?.focus();
} }
}; };
const colorsBlocks = [
colors.pastelPink,
colors.pastelBlue,
colors.pastelGreen,
colors.pastelOrange,
colors.pastelBlue,
colors.pastelPink,
colors.pastelGreen,
colors.pastelOrange,
colors.pastelPink,
];
return ( return (
<LinearGradient colors={[colors.pastelGreen, colors.pastelOrange]} style={styles.gradientContainer}> <View style={styles.container}>
<KeyboardAvoidingView style={styles.container} behavior={Platform.OS === "ios" ? "padding" : undefined}> <StatusBar barStyle="dark-content" backgroundColor={colors.pastelOrange} />
<StatusBar barStyle="dark-content" backgroundColor={colors.pastelGreen} />
<View style={styles.gridContainer}>
{colorsBlocks.map((bgColor, index) => (
<View
key={index}
style={[
styles.gridBlock,
{ backgroundColor: bgColor },
index % 2 === 0 ? styles.elevatedBlock : {},
]}
/>
))}
</View>
<KeyboardAvoidingView style={styles.keyboardAvoidingContainer} behavior={Platform.OS === "ios" ? "padding" : undefined}>
<CustomAlert {...alertConfig} /> <CustomAlert {...alertConfig} />
<View style={styles.topSection}> <View style={styles.contentPanel}>
<Text style={styles.title}>Verify Your Code 🔒</Text> <View style={styles.topSection}>
<Text style={styles.subtitle}>We’ve sent a 6-digit verification code to +91 {mobile}.</Text> <Text style={styles.title}>Verify Your Code</Text>
<Text style={styles.subtitle}>We’ve sent a 6-digit verification code to +91 {mobile}.</Text>
<Animated.View style={[styles.otpRow, { opacity: fade, transform: [{ translateY: slide }] }]}> <Animated.View style={[styles.otpRow, { opacity: fade, transform: [{ translateY: slide }] }]}>
{Array.from({ length: CELL_COUNT }).map((_, i) => ( {Array.from({ length: CELL_COUNT }).map((_, i) => (
<TextInput <TextInput
key={`otp-${i}`} key={`otp-${i}`}
ref={(r) => { inputs.current[i] = r; }} ref={(r) => { inputs.current[i] = r; }}
style={[styles.cell, code[i] ? styles.cellFilled : null]} style={[styles.cell, code[i] ? styles.cellFilled : null]}
keyboardType="number-pad" keyboardType="number-pad"
maxLength={i === 0 ? CELL_COUNT : 1} maxLength={i === 0 ? CELL_COUNT : 1}
value={code[i]} value={code[i]}
onChangeText={(t) => (i === 0 ? handleFirstCellChange(t) : updateIndex(i, t))} onChangeText={(t) => (i === 0 ? handleFirstCellChange(t) : updateIndex(i, t))}
onKeyPress={(e) => handleKeyPress(i, e)} onKeyPress={(e) => handleKeyPress(i, e)}
placeholder="•" placeholder="•"
placeholderTextColor="#BDBDBD" placeholderTextColor="#BDBDBD"
textContentType="oneTimeCode" textContentType="oneTimeCode"
/> />
))} ))}
</Animated.View>
</View>
<Animated.View style={[styles.bottomSection, { transform: [{ scale: ctaScale }] }]}>
<Pressable
onPressIn={() => Animated.spring(ctaScale, { toValue: 0.97, useNativeDriver: true }).start()}
onPressOut={() => Animated.spring(ctaScale, { toValue: 1, useNativeDriver: true }).start()}
onPress={() => onSubmit(code.join(""), mobile)}
style={styles.button}
>
<Text style={styles.buttonText}>Verify & Continue</Text>
</Pressable>
{timer > 0 ? (
<Text style={styles.footerText}>Resend code in 00:{timer.toString().padStart(2, "0")}</Text>
) : (
<Pressable onPress={resendOtp} disabled={isResending}>
<Text style={[styles.footerText, { color: colors.pastelPink, fontWeight: '600' }]}>
{isResending ? "Resending..." : "Resend Code"}
</Text>
</Pressable>
)}
</Animated.View> </Animated.View>
</View> </View>
<Animated.View style={[styles.bottomSection, { transform: [{ scale: ctaScale }] }]}>
<Pressable
onPressIn={() => Animated.spring(ctaScale, { toValue: 0.97, useNativeDriver: true }).start()}
onPressOut={() => Animated.spring(ctaScale, { toValue: 1, useNativeDriver: true }).start()}
onPress={() => onSubmit(code.join(""), mobile)}
style={styles.button}
>
<Text style={styles.buttonText}>Verify & Continue</Text>
</Pressable>
{timer > 0 ? (
<Text style={styles.footerText}>Resend code in 00:{timer.toString().padStart(2, "0")}</Text>
) : (
<Pressable onPress={resendOtp} disabled={isResending}>
<Text style={[styles.footerText, { color: colors.primary, fontWeight: '600' }]}>
{isResending ? "Resending..." : "Resend Code"}
</Text>
</Pressable>
)}
</Animated.View>
</KeyboardAvoidingView> </KeyboardAvoidingView>
</LinearGradient> </View>
); );
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
gradientContainer: { flex: 1 }, container: { flex: 1 },
container: { flex: 1, padding: 24, justifyContent: "space-between" }, gridContainer: {
topSection: { flex: 1, justifyContent: "center", alignItems: 'center' }, ...StyleSheet.absoluteFillObject,
flexDirection: 'row',
flexWrap: 'wrap',
},
gridBlock: {
width: BLOCK_SIZE,
height: BLOCK_SIZE,
margin: 2,
borderRadius: 16,
},
elevatedBlock: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 5 },
shadowOpacity: 0.15,
shadowRadius: 10,
elevation: 7,
},
keyboardAvoidingContainer: { flex: 1, justifyContent: 'center' },
contentPanel: {
marginHorizontal: 24,
backgroundColor: 'rgba(255, 255, 255, 0.95)',
borderRadius: 32,
padding: 28,
shadowColor: '#000',
shadowOffset: { width: 0, height: 10 },
shadowOpacity: 0.14,
shadowRadius: 24,
elevation: 12,
},
topSection: { alignItems: 'center', marginBottom: 24 },
title: { fontSize: 28, fontWeight: "bold", color: colors.textDark, textAlign: 'center', marginBottom: 12 }, title: { fontSize: 28, fontWeight: "bold", color: colors.textDark, textAlign: 'center', marginBottom: 12 },
subtitle: { fontSize: 16, color: colors.textLight, textAlign: 'center', marginBottom: 32, lineHeight: 24, maxWidth: 300 }, subtitle: { fontSize: 16, color: colors.textLight, textAlign: 'center', marginBottom: 32, lineHeight: 24, maxWidth: 300 },
otpRow: { flexDirection: "row", justifyContent: "space-between", width: '100%', paddingHorizontal: 10 }, otpRow: { flexDirection: "row", justifyContent: "space-between", width: '100%', paddingHorizontal: 10 },
cell: { width: 48, height: 56, borderRadius: 12, borderWidth: 1.5, borderColor: colors.inputBorder, backgroundColor: colors.white, textAlign: "center", fontSize: 22, fontWeight: '700', color: colors.textDark, shadowColor: "#000", shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.08, shadowRadius: 8, elevation: 3 }, cell: { width: 48, height: 56, borderRadius: 12, borderWidth: 1.5, borderColor: colors.inputBorder, backgroundColor: colors.white, textAlign: "center", fontSize: 22, fontWeight: '700', color: colors.textDark, shadowColor: "#000", shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.08, shadowRadius: 8, elevation: 3 },
cellFilled: { borderColor: colors.primary, backgroundColor: '#F7F2FF' }, cellFilled: { borderColor: colors.primary, backgroundColor: '#F7F2FF' },
bottomSection: { paddingBottom: Platform.OS === "ios" ? 20 : 10 }, bottomSection: { paddingBottom: Platform.OS === "ios" ? 20 : 10, paddingTop: 10 },
button: { backgroundColor: colors.primary, paddingVertical: 18, borderRadius: 30, alignItems: "center", justifyContent: "center", shadowColor: colors.primary, shadowOffset: { width: 0, height: 8 }, shadowOpacity: 0.3, shadowRadius: 15, elevation: 8 }, button: { backgroundColor: colors.pastelPink, paddingVertical: 18, borderRadius: 30, alignItems: "center", justifyContent: "center", shadowColor: colors.primary, shadowOffset: { width: 0, height: 8 }, shadowOpacity: 0.3, shadowRadius: 15, elevation: 8 ,borderWidth:2, borderColor:colors.pastelPink},
buttonText: { color: colors.white, fontWeight: "700", fontSize: 16 }, buttonText: { color: colors.white, fontWeight: "700", fontSize: 16 },
footerText: { textAlign: "center", color: colors.textLight, fontSize: 14, marginTop: 20 }, footerText: { textAlign: "center", color: colors.textLight, fontSize: 14, marginTop: 20 },
}); });
+1 -1
View File
@@ -110,7 +110,7 @@ const SplashScreen = () => {
<Animated.Text style={[styles.title, { opacity: fadeAnim }]}> <Animated.Text style={[styles.title, { opacity: fadeAnim }]}>
Circle Collect Circle Collect
</Animated.Text> </Animated.Text>
<ActivityIndicator size="large" color={colors.Home} style={{ marginTop: 20 }} /> <ActivityIndicator size="large" color={colors.pastelPink} style={{ marginTop: 20 }} />
</View> </View>
); );
}; };
+2
View File
@@ -60,6 +60,7 @@ const userActions: ActionItem[] = [
{ id: "1", title: "Joined Groups", subtitle: "See your groups", action: "NAVIGATE_MANAGEMENT", tab: "Joined Groups", iconName: "person-outline", backOne: '#D7E9F7', backTwo: '#82AAE3' }, { id: "1", title: "Joined Groups", subtitle: "See your groups", action: "NAVIGATE_MANAGEMENT", tab: "Joined Groups", iconName: "person-outline", backOne: '#D7E9F7', backTwo: '#82AAE3' },
{ id: "2", title: "Your Requests", subtitle: "Track your requests", action: "NAVIGATE_MANAGEMENT", tab: "Your Requests", iconName: "mail-open-outline", backOne: '#FFF4D2', backTwo: '#FFB344' }, { id: "2", title: "Your Requests", subtitle: "Track your requests", action: "NAVIGATE_MANAGEMENT", tab: "Your Requests", iconName: "mail-open-outline", backOne: '#FFF4D2', backTwo: '#FFB344' },
{ id: "3", title: "Transactions", subtitle: "View your history", action: "TRANSACTION", iconName: "receipt-outline", backOne: '#decbfcff', backTwo: '#B799FF' }, { id: "3", title: "Transactions", subtitle: "View your history", action: "TRANSACTION", iconName: "receipt-outline", backOne: '#decbfcff', backTwo: '#B799FF' },
{ id: "4", title: "Create Group", subtitle: "Create a new group", action: "NAVIGATE_CREATE_GROUP", tab: "Requests", iconName: "people-outline", backOne: '#DFF6DD', backTwo: '#88D18A' },
]; ];
const Home: React.FC = () => { const Home: React.FC = () => {
@@ -96,6 +97,7 @@ const Home: React.FC = () => {
} else if (item.action === "TRANSACTION") { } else if (item.action === "TRANSACTION") {
navigation.navigate("TransactionList" as never); navigation.navigate("TransactionList" as never);
} else if (item.action === "NAVIGATE_CREATE_GROUP") { } else if (item.action === "NAVIGATE_CREATE_GROUP") {
navigation.navigate("GroupCreation" as never); navigation.navigate("GroupCreation" as never);
} else { } else {
ToastAndroid.show(`Unknown action: ${item.title}`, ToastAndroid.SHORT); ToastAndroid.show(`Unknown action: ${item.title}`, ToastAndroid.SHORT);
@@ -164,7 +164,7 @@ const CycleDashboard = ({ data }: { data: CycleDashboardData }) => {
</View> </View>
<View style={styles.summaryBox}> <View style={styles.summaryBox}>
<Text style={styles.summaryLabel}>Cycles</Text> <Text style={styles.summaryLabel}>Cycles</Text>
<Text style={styles.summaryValue}>{data.currentCycle}/{data.totalCycles}</Text> <Text style={styles.summaryValue}>{data.currentCycle}/{data.totalParticipants}</Text>
</View> </View>
</View> </View>
</View> </View>
+82 -18
View File
@@ -12,16 +12,18 @@ import {
import { useRoute } 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';
import Header from "../../../components/common/Header"; 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"; import CycleDashboard from "./CycleDashboard";
const CustomDropdown = ({ items, selectedValue, onValueChange, placeholder = "Select an item" }:any) => {
const CustomDropdown = ({ items, selectedValue, onValueChange, placeholder = "Select an item" }: any) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const selectedLabel = items.find((item:any) => item.value === selectedValue)?.label || placeholder; const selectedLabel = items.find((item: any) => item.value === selectedValue)?.label || placeholder;
return ( return (
@@ -31,6 +33,7 @@ const CustomDropdown = ({ items, selectedValue, onValueChange, placeholder = "Se
<Ionicons name='chevron-down' size={22} color={colors.black} /> <Ionicons name='chevron-down' size={22} color={colors.black} />
</TouchableOpacity> </TouchableOpacity>
<Modal <Modal
transparent transparent
animationType="fade" animationType="fade"
@@ -40,7 +43,7 @@ const CustomDropdown = ({ items, selectedValue, onValueChange, placeholder = "Se
<TouchableOpacity style={styles.modalOverlay} onPress={() => setOpen(false)}> <TouchableOpacity style={styles.modalOverlay} onPress={() => setOpen(false)}>
<View style={styles.dropdownListContainer}> <View style={styles.dropdownListContainer}>
<ScrollView> <ScrollView>
{items.map(item => ( {items.map((item: any) => (
<TouchableOpacity <TouchableOpacity
key={item.value} key={item.value}
onPress={() => { onPress={() => {
@@ -63,9 +66,11 @@ const CustomDropdown = ({ items, selectedValue, onValueChange, placeholder = "Se
); );
}; };
const GroupDetails = () => { const GroupDetails = () => {
const route = useRoute<any>(); const route = useRoute<any>();
console.log(" route id : ", route) console.log(" route id : ", route)
const [memberData, setMemberData] = useState([]); const [memberData, setMemberData] = useState([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -75,6 +80,8 @@ const GroupDetails = () => {
const [cycleDashboardData, setCycleDashboardData] = useState<any>(); const [cycleDashboardData, setCycleDashboardData] = useState<any>();
const [selectedCycleNumber, setSelectedCycleNumber] = useState(null); const [selectedCycleNumber, setSelectedCycleNumber] = useState(null);
const [cycleItems, setCycleItems] = useState([]); const [cycleItems, setCycleItems] = useState([]);
const [selectedUser, setSelectedUser] = useState() as any
const group = { const group = {
id: route.params.item?.id, id: route.params.item?.id,
@@ -86,14 +93,26 @@ const GroupDetails = () => {
members: memberData, members: memberData,
}; };
const getPaymentStatus = async () => {
const response = await apiCall(`${BASE_URL}payments/group/${group.id || route?.params?.groupId}/payment/paid`, 'GET') as { status: string; data?: { currentCycle: number, allPaid: boolean, selectedUser: any }; message?: string };
console.log("paymentStatus", response.data?.selectedUser)
if (response.status === 'success') {
setSelectedUser(response.data?.selectedUser);
}
}
const getCycleDashboard = async () => { const getCycleDashboard = async () => {
try { try {
const response = await apiCall(`${BASE_URL}payments/groups/${group.id || route?.params?.groupId}/cycle-dashboard`, "GET") as any; const response = await apiCall(`${BASE_URL}payments/groups/${group.id || route?.params?.groupId}/cycle-dashboard`, "GET") as any;
const responseData = response.data; const responseData = response.data;
setCycleDashboardData(responseData); setCycleDashboardData(responseData);
console.log("cycledata", responseData)
if (responseData?.cycles) { if (responseData?.cycles) {
const items = responseData.cycles.map(cycle => ({ const items = responseData.cycles.map((cycle: any) => ({
label: `Cycle ${cycle.cycleNumber}`, label: `Cycle ${cycle.cycleNumber}`,
value: cycle.cycleNumber value: cycle.cycleNumber
})); }));
@@ -107,6 +126,7 @@ const GroupDetails = () => {
} }
}; };
const fetchGroupDetails = async () => { const fetchGroupDetails = async () => {
try { try {
const res = await apiCall(`${GROUP_ALL_MEMBER}?groupId=${group.id || route?.params?.groupId}`, "GET") as any; const res = await apiCall(`${GROUP_ALL_MEMBER}?groupId=${group.id || route?.params?.groupId}`, "GET") as any;
@@ -117,6 +137,7 @@ const GroupDetails = () => {
} }
}; };
const handleContribution = async () => { const handleContribution = async () => {
try { try {
const res = await apiCall(`${CONTRIBUTE}${group.id || route?.params?.groupId}/calculate-contribution`, 'GET') as any; const res = await apiCall(`${CONTRIBUTE}${group.id || route?.params?.groupId}/calculate-contribution`, 'GET') as any;
@@ -126,6 +147,7 @@ const GroupDetails = () => {
} }
}; };
const fetchAllData = useCallback(async () => { const fetchAllData = useCallback(async () => {
setLoading(true); setLoading(true);
await Promise.all([ await Promise.all([
@@ -136,22 +158,28 @@ const GroupDetails = () => {
setLoading(false); setLoading(false);
}, [group.id || route?.params?.groupId]); }, [group.id || route?.params?.groupId]);
useEffect(() => { useEffect(() => {
if (group.id || route?.params?.groupId) { if (group.id || route?.params?.groupId) {
fetchAllData(); fetchAllData();
getPaymentStatus();
} }
}, [group.id || route?.params?.groupId, fetchAllData]); }, [group.id || route?.params?.groupId, fetchAllData]);
const onRefresh = useCallback(async () => { const onRefresh = useCallback(async () => {
setRefreshing(true); setRefreshing(true);
await fetchAllData(); await fetchAllData();
await getPaymentStatus();
setRefreshing(false); setRefreshing(false);
}, [fetchAllData]); }, [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;
if (res.status === 200 || res.status === 201) { console.log('payment res ', res)
if (res.status === 200 || res.status === 201 || res.statusCode === 200) {
ToastAndroid.show('Payment done!', ToastAndroid.SHORT); ToastAndroid.show('Payment done!', ToastAndroid.SHORT);
onRefresh(); onRefresh();
} else { } else {
@@ -162,10 +190,19 @@ const GroupDetails = () => {
} }
}; };
const selectedCycleData = cycleDashboardData?.cycles.find(c => c.cycleNumber === selectedCycleNumber);
const selectedCycleData = cycleDashboardData?.cycles.find((c: any) => c.cycleNumber === selectedCycleNumber);
return ( return (
<AppView paddingHorizontal={0} style={{ flex: 1 }}> <AppView paddingHorizontal={0} style={{ flex: 1 }}>
{selectedUser?.firstName && (
<View style={styles.floatingWinnerContainer}>
<Text style={styles.floatingWinnerText}>
Winner: {`${selectedUser.firstName} ${selectedUser.lastName || ''}`}
</Text>
</View>
)}
<ScrollView <ScrollView
refreshControl={ refreshControl={
<RefreshControl refreshing={refreshing || loading} onRefresh={onRefresh} colors={[colors.black]} /> <RefreshControl refreshing={refreshing || loading} onRefresh={onRefresh} colors={[colors.black]} />
@@ -175,21 +212,23 @@ const GroupDetails = () => {
<Header title={group.name || route.name} /> <Header title={group.name || route.name} />
<Text style={styles.description}>{group.description || route.name}</Text> <Text style={styles.description}>{group.description || route.name}</Text>
<View style={styles.statsRow}> <View style={styles.statsRow}>
<View style={styles.statBox}> <View style={styles.statBox}>
<Text style={styles.statValue}>{group.members.length ?? 0}</Text> <Text style={styles.statValue}>{memberData.length ?? 0}</Text>
<Text style={styles.statLabel}>Members</Text> <Text style={styles.statLabel}>Members</Text>
</View> </View>
<View style={styles.statBox}> <View style={styles.statBox}>
<Text style={styles.statValue}>₹{group.collected}</Text> <Text style={styles.statValue}>₹{cycleDashboardData?.contributionAmount || group.collected}</Text>
<Text style={styles.statLabel}>Contribution</Text> <Text style={styles.statLabel}>Contribution</Text>
</View> </View>
<View style={styles.statBox}> <View style={styles.statBox}>
<Text style={styles.statValue}>₹{group.goal}</Text> <Text style={styles.statValue}>₹{cycleDashboardData?.contributionAmount * memberData.length || group.goal}</Text>
<Text style={styles.statLabel}>Total Amount</Text> <Text style={styles.statLabel}>Total Amount</Text>
</View> </View>
</View> </View>
{cycleItems.length > 0 && ( {cycleItems.length > 0 && (
<CustomDropdown <CustomDropdown
items={cycleItems} items={cycleItems}
@@ -198,19 +237,21 @@ const GroupDetails = () => {
placeholder="Select a Cycle" placeholder="Select a Cycle"
/> />
)} )}
{selectedCycleData && cycleDashboardData && ( {selectedCycleData && cycleDashboardData && (
<CycleDashboard data={{ ...cycleDashboardData, cycles: [selectedCycleData] }} /> <CycleDashboard data={{ ...cycleDashboardData, cycles: [selectedCycleData] }} />
)} )}
<View style={styles.membersHeader}> <View style={styles.membersHeader}>
<Text style={styles.sectionTitle}>Members</Text> <Text style={styles.sectionTitle}>Members</Text>
<View style={styles.statusBadge}> <View style={styles.statusBadge}>
<Text style={styles.statusText}>{group.status}</Text> <Text style={styles.statusText}>{cycleDashboardData?.status || group?.status}</Text>
</View> </View>
</View> </View>
{memberData.map((member:any) => (
{memberData.map((member: any) => (
<View key={member.userId} style={styles.memberCard}> <View key={member.userId} style={styles.memberCard}>
<Ionicons name="person" size={40} color={colors.black} /> <Ionicons name="person" size={40} color={colors.black} />
<View style={styles.memberInfo}> <View style={styles.memberInfo}>
@@ -221,14 +262,15 @@ const GroupDetails = () => {
</View> </View>
))} ))}
{/* Button at the bottom inside scroll (no absolute positioning) */}
{group.status === 'ACTIVE' && !payable?.hasPaid && ( {cycleDashboardData?.status === 'ACTIVE' && !payable?.hasPaid && (
<TouchableOpacity onPress={() => setOpenModal(true)} style={styles.button}> <TouchableOpacity onPress={() => setOpenModal(true)} style={styles.button}>
<Text style={styles.buttonText}>Contribute</Text> <Text style={styles.buttonText}>Contribute</Text>
</TouchableOpacity> </TouchableOpacity>
)} )}
</ScrollView> </ScrollView>
{openModal && ( {openModal && (
<ContributionScreen <ContributionScreen
data={payable} data={payable}
@@ -244,8 +286,10 @@ const GroupDetails = () => {
); );
}; };
export default GroupDetails; export default GroupDetails;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
dropdownWrapper: { dropdownWrapper: {
marginHorizontal: 15, marginHorizontal: 15,
@@ -261,6 +305,10 @@ const styles = StyleSheet.create({
borderRadius: 8, borderRadius: 8,
backgroundColor: '#fff', backgroundColor: '#fff',
}, },
headerText: {
fontSize: 15,
color: colors.black,
},
modalOverlay: { modalOverlay: {
flex: 1, flex: 1,
justifyContent: 'center', justifyContent: 'center',
@@ -335,7 +383,7 @@ const styles = StyleSheet.create({
statusText: { statusText: {
fontSize: 14, fontSize: 14,
fontWeight: '600', fontWeight: '600',
color: colors.Home, color: colors.black,
}, },
memberCard: { memberCard: {
flexDirection: "row", flexDirection: "row",
@@ -373,4 +421,20 @@ const styles = StyleSheet.create({
scrollContent: { scrollContent: {
paddingBottom: 50, paddingBottom: 50,
}, },
floatingWinnerContainer: {
position: 'absolute',
bottom: 80,
alignSelf: 'center',
backgroundColor: colors.pastelBlue,
paddingHorizontal: 15,
paddingVertical: 8,
borderRadius: 20,
elevation: 5,
zIndex: 10,
},
floatingWinnerText: {
color: colors.black,
fontWeight: 'bold',
fontSize: 16,
},
}); });
+58 -41
View File
@@ -32,6 +32,7 @@ import { StackNavigationProp } from '@react-navigation/stack';
import WinnerUser from './WinnerUser'; import WinnerUser from './WinnerUser';
// --- Type Definitions --- // --- Type Definitions ---
interface Participant { interface Participant {
id: string; id: string;
@@ -42,6 +43,7 @@ interface Participant {
} }
export interface GroupDetails { export interface GroupDetails {
id: string; id: string;
groupName: string; groupName: string;
@@ -63,15 +65,16 @@ export interface GroupDetails {
organizerId: string; organizerId: string;
totalParticipants: number; totalParticipants: number;
participants: Participant[]; participants: Participant[];
imageBlob: string imageBlob: string;
} }
export interface EditGroupPayload { export interface EditGroupPayload {
groupName: string; groupName: string;
description: string; description: string;
maxParticipants: number; maxParticipants: number;
minParticipants?:number; minParticipants?: number;
contributionAmount: number; contributionAmount: number;
customCycleDays: number | null; customCycleDays: number | null;
cycleType: 'QUARTERLY' | 'MONTHLY' | 'WEEKLY' | 'CUSTOM'; cycleType: 'QUARTERLY' | 'MONTHLY' | 'WEEKLY' | 'CUSTOM';
@@ -82,6 +85,7 @@ export interface EditGroupPayload {
} }
// Correct props for a screen inside a nested Tab Navigator // Correct props for a screen inside a nested Tab Navigator
type EditGroupScreenProps = CompositeScreenProps< type EditGroupScreenProps = CompositeScreenProps<
BottomTabScreenProps<RootStackParamList, 'EditGroup'>, BottomTabScreenProps<RootStackParamList, 'EditGroup'>,
@@ -89,16 +93,20 @@ type EditGroupScreenProps = CompositeScreenProps<
>; >;
type EditingCard = 'groupInfo' | 'financialRules' | 'configuration' | null; type EditingCard = 'groupInfo' | 'financialRules' | 'configuration' | null;
type GroupManagementNavProp = StackNavigationProp<RootStackParamList, 'EditGroup'>; type GroupManagementNavProp = StackNavigationProp<RootStackParamList, 'EditGroup'>;
// --- Main Screen Component --- // --- Main Screen Component ---
const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => { const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
const { groupId } = route?.params; const { groupId } = route?.params;
console.log("edit group screen groupId : ", route?.params)
console.log("edit group screen groupId : ", route?.params)
const navigation = useNavigation<GroupManagementNavProp>(); const navigation = useNavigation<GroupManagementNavProp>();
const [editingCard, setEditingCard] = useState<EditingCard>(null); const [editingCard, setEditingCard] = useState<EditingCard>(null);
const [groupDetails, setGroupDetails] = useState<GroupDetails | null>(null); const [groupDetails, setGroupDetails] = useState<GroupDetails | null>(null);
const [editableData, setEditableData] = useState<EditGroupPayload>({ const [editableData, setEditableData] = useState<EditGroupPayload>({
@@ -114,6 +122,7 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
lateFeeValue: null, lateFeeValue: null,
}); });
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [starting, setStarting] = useState(false); const [starting, setStarting] = useState(false);
@@ -141,6 +150,7 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
}; };
const fetchGroupDetails = useCallback(async () => { const fetchGroupDetails = useCallback(async () => {
setLoading(true); setLoading(true);
setError(null); setError(null);
@@ -148,6 +158,7 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
const response = await apiCall(`${BASE_URL}fund/group/details?groupId=${groupId}`, 'GET') as { status: string; data?: GroupDetails; message?: string }; const response = await apiCall(`${BASE_URL}fund/group/details?groupId=${groupId}`, 'GET') as { status: string; data?: GroupDetails; message?: string };
console.log("groupDetails", response) console.log("groupDetails", response)
if (response.status === 'success' && response.data) { if (response.status === 'success' && response.data) {
setGroupDetails(response.data); setGroupDetails(response.data);
initializeEditableData(response.data); initializeEditableData(response.data);
@@ -160,14 +171,6 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
setLoading(false); setLoading(false);
} }
}, [groupId]); }, [groupId]);
// const getPaymentStatus = async()=>{
// const response = await apiCall(`${BASE_URL}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);
// }
// }
const getPaymentStatus = async () => { const getPaymentStatus = async () => {
const response = await apiCall(`${BASE_URL}payments/group/${groupId}/payment/paid`, 'GET') as { status: string; data?: { currentCycle: number, allPaid: boolean, selectedUser: any }; message?: string }; const response = await apiCall(`${BASE_URL}payments/group/${groupId}/payment/paid`, 'GET') as { status: string; data?: { currentCycle: number, allPaid: boolean, selectedUser: any }; message?: string };
@@ -179,16 +182,19 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
} }
} }
console.log("groupid : ", groupId) console.log("groupid : ", groupId)
useEffect(() => { useEffect(() => {
getPaymentStatus() getPaymentStatus()
}, []) }, [])
useEffect(() => { useEffect(() => {
fetchGroupDetails(); fetchGroupDetails();
}, [fetchGroupDetails]); }, [fetchGroupDetails]);
const handleCancel = () => { const handleCancel = () => {
if (groupDetails) { if (groupDetails) {
initializeEditableData(groupDetails); initializeEditableData(groupDetails);
@@ -197,10 +203,10 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
}; };
const handleSave = async () => { const handleSave = async () => {
if (!groupDetails) return; if (!groupDetails) return;
if (editableData.maxParticipants < groupDetails.totalParticipants) { if (editableData.maxParticipants < groupDetails.totalParticipants) {
ToastAndroid.show(`Max participants cannot be less than the current count (${groupDetails.totalParticipants}).`, ToastAndroid.SHORT); ToastAndroid.show(`Max participants cannot be less than the current count (${groupDetails.totalParticipants}).`, ToastAndroid.SHORT);
return; return;
@@ -208,8 +214,10 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
setSaving(true); setSaving(true);
try { try {
await apiCall(`${BASE_URL}fund/groups/${groupDetails.id}/edit`, 'PUT', editableData); const res = await apiCall(`${BASE_URL}fund/groups/${groupDetails.id}/edit`, 'PUT', editableData);
ToastAndroid.show('Group has been updated.', ToastAndroid.SHORT); console.log("Edit Group Response: ", res,editableData)
Alert.alert('Success', 'Group has been updated.');
setEditingCard(null); setEditingCard(null);
await fetchGroupDetails(); await fetchGroupDetails();
} catch (e: any) { } catch (e: any) {
@@ -220,16 +228,19 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
}; };
const handleDistribute = async () => { const handleDistribute = async () => {
navigation.navigate('RandomSelectUserScreen', { groupId , cycleNumber: currentCycle || 0 }); navigation.navigate('RandomSelectUserScreen', { groupId, cycleNumber: currentCycle || 0 });
}; };
const handleStartGroup = async () => { const handleStartGroup = async () => {
if (!groupDetails) return; if (!groupDetails) return;
Alert.alert( Alert.alert(
"Confirm Start", "Confirm Start",
"Are you sure you want to start this group? This action cannot be undone.", "Are you sure you want to start this group? This action cannot be undone.",
@@ -256,13 +267,13 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
); );
}; };
const handleInvite = async () => { const handleInvite = async () => {
if (!groupDetails) return; if (!groupDetails) return;
setInviting(true); setInviting(true);
try { try {
const response = await apiCall(`${BASE_URL}invite/generate?groupId=${groupDetails.id}`, 'GET') as { data?: string }; const response = await apiCall(`${BASE_URL}invite/generate?groupId=${groupDetails.id}`, 'GET') as { data?: string };
const inviteLink = response.data; const inviteLink = response.data;
if (inviteLink) { if (inviteLink) {
@@ -284,12 +295,16 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
}; };
const handleUpdateField = (field: keyof EditGroupPayload, value: any) => { const handleUpdateField = (field: keyof EditGroupPayload, value: any) => {
setEditableData(prev => ({ ...prev, [field]: value })); setEditableData(prev => ({ ...prev, [field]: value }));
}; };
const renderField = (field: keyof EditGroupPayload, label: string, displayValue: string | number, isEditing: boolean, isEditable?: boolean) => { // --- CORRECTED FUNCTION ---
// The 'isEditable' parameter was removed, and the condition now correctly
// uses 'isEditing' to show the input fields.
const renderField = (field: keyof EditGroupPayload, label: string, displayValue: string | number, isEditing: boolean) => {
const commonInputProps = { style: styles.input, placeholderTextColor: "#9CA3AF" }; const commonInputProps = { style: styles.input, placeholderTextColor: "#9CA3AF" };
const getInputForField = () => { const getInputForField = () => {
@@ -328,7 +343,7 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
return ( return (
<View style={styles.fieldContainer}> <View style={styles.fieldContainer}>
<Text style={styles.infoLabel}>{label}</Text> <Text style={styles.infoLabel}>{label}</Text>
{isEditing && isEditable ? ( {isEditing ? (
<View style={{ marginTop: 8 }}>{getInputForField()}</View> <View style={{ marginTop: 8 }}>{getInputForField()}</View>
) : ( ) : (
<Text style={styles.infoValue}>{displayValue}</Text> <Text style={styles.infoValue}>{displayValue}</Text>
@@ -337,9 +352,11 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
); );
} }
const renderCard = (title: string, fields: Array<{ field: keyof EditGroupPayload, label: string, displayValue: () => string | number }>, cardKey: EditingCard) => { const renderCard = (title: string, fields: Array<{ field: keyof EditGroupPayload, label: string, displayValue: () => string | number }>, cardKey: EditingCard) => {
const isEditingThisCard = editingCard === cardKey; const isEditingThisCard = editingCard === cardKey;
// This isEditable check correctly controls the visibility of the "Update" button itself.
const isEditable = groupDetails.status === 'PENDING' || groupDetails.status === 'APPROVED';
return ( return (
<View style={styles.card}> <View style={styles.card}>
@@ -353,6 +370,7 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
</View> </View>
{fields.map(({ field, label, displayValue }) => {fields.map(({ field, label, displayValue }) =>
// The call now correctly matches the updated renderField signature
renderField(field, label, displayValue(), isEditingThisCard) renderField(field, label, displayValue(), isEditingThisCard)
)} )}
@@ -370,15 +388,16 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
); );
} }
if (loading) return <View style={styles.centered}><ActivityIndicator size="large" color="#4F46E5" /></View>; if (loading) return <View style={styles.centered}><ActivityIndicator size="large" color="#4F46E5" /></View>;
if (error) return <View style={styles.centered}><Text style={styles.errorText}>{error}</Text></View>; if (error) return <View style={styles.centered}><Text style={styles.errorText}>{error}</Text></View>;
if (!groupDetails) return null; if (!groupDetails) return null;
const isEditable = groupDetails.status === 'PENDING' || groupDetails.status === 'APPROVED';
const isInvitable = groupDetails.status === 'APPROVED'; const isInvitable = groupDetails.status === 'APPROVED';
const canStartGroup = groupDetails.status === 'APPROVED' && groupDetails.totalParticipants >= groupDetails.minParticipants; const canStartGroup = groupDetails.status === 'APPROVED' && groupDetails.totalParticipants >= groupDetails.minParticipants;
console.log(" myu status : ", groupDetails.status) console.log(" myu status : ", groupDetails.status)
const pickImage = () => { const pickImage = () => {
@@ -399,7 +418,6 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
const maxSizeKB = 1000; const maxSizeKB = 1000;
const maxSizeBytes = maxSizeKB * 1024; const maxSizeBytes = maxSizeKB * 1024;
// Size validation
if (selectedImage.fileSize && selectedImage.fileSize > maxSizeBytes) { if (selectedImage.fileSize && selectedImage.fileSize > maxSizeBytes) {
const currentSizeKB = (selectedImage.fileSize / 1024).toFixed(2); const currentSizeKB = (selectedImage.fileSize / 1024).toFixed(2);
Alert.alert( Alert.alert(
@@ -409,23 +427,15 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
return; return;
} }
// Additional validation for required fields
if (!selectedImage.uri) { if (!selectedImage.uri) {
ToastAndroid.show("Invalid image file", ToastAndroid.SHORT); ToastAndroid.show("Invalid image file", ToastAndroid.SHORT);
return; return;
} }
// console.log("✅ Image selected:", {
// uri: selectedImage.uri,
// size: (selectedImage?.fileSize / 1024).toFixed(2) + "KB",
// width: selectedImage.width,
// height: selectedImage.height
// });
setSelectedImage(selectedImage.uri); setSelectedImage(selectedImage.uri);
}); });
}; };
const handleUpdateImage = async () => { const handleUpdateImage = async () => {
try { try {
const credentials = await Keychain.getGenericPassword(); const credentials = await Keychain.getGenericPassword();
@@ -435,16 +445,18 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
} }
const token = credentials.password; const token = credentials.password;
if (!selectedImage) { if (!selectedImage) {
ToastAndroid.show("Please select an image first", ToastAndroid.SHORT); ToastAndroid.show("Please select an image first", ToastAndroid.SHORT);
return; return;
} }
setImageUpdating(true); setImageUpdating(true);
// Get file extension and mime type dynamically
const fileExtension = selectedImage.split('.').pop(); const fileExtension = selectedImage.split('.').pop();
const formData = new FormData(); const formData = new FormData();
formData.append("file", { formData.append("file", {
uri: selectedImage, uri: selectedImage,
@@ -452,12 +464,13 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
name: `group_${groupId}_profile.${fileExtension || "jpg"}`, name: `group_${groupId}_profile.${fileExtension || "jpg"}`,
}); });
const url = `${UPLOAD_GROUP_IMAGE}${groupId}/upload-image`; const url = `${UPLOAD_GROUP_IMAGE}${groupId}/upload-image`;
console.log("url : ", url) console.log("url : ", url)
console.log("formdata : ", formData) console.log("formdata : ", formData)
// return
const response = await fetch(url, { const response = await fetch(url, {
method: "PUT", method: "PUT",
headers: { headers: {
@@ -467,12 +480,14 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
body: formData, body: formData,
}); });
console.log("image response : ", response) console.log("image response : ", response)
if (response?.status === 200) { if (response?.status === 200) {
fetchGroupDetails() fetchGroupDetails()
ToastAndroid.show('Group uploaded successfully', ToastAndroid.SHORT) ToastAndroid.show('Group uploaded successfully', ToastAndroid.SHORT)
} }
} catch (error: any) { } catch (error: any) {
console.error("❌ Error updating group image:", error); console.error("❌ Error updating group image:", error);
} finally { } finally {
@@ -480,6 +495,7 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
} }
}; };
const getProfileImage = () => { const getProfileImage = () => {
return selectedImage return selectedImage
? selectedImage ? selectedImage
@@ -487,8 +503,6 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
? `data:image/jpeg;base64,${groupDetails.imageBlob}` ? `data:image/jpeg;base64,${groupDetails.imageBlob}`
: "https://img.freepik.com/premium-vector/people-group-frame-icon_791764-2649.jpg"; : "https://img.freepik.com/premium-vector/people-group-frame-icon_791764-2649.jpg";
}; };
// Helper function to get MIME type
return ( return (
<AppView style={{ flex: 1, paddingHorizontal: 0 }}> <AppView style={{ flex: 1, paddingHorizontal: 0 }}>
@@ -501,7 +515,6 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
)} )}
</View> </View>
<ScrollView <ScrollView
style={styles.container} style={styles.container}
contentContainerStyle={styles.contentContainer} contentContainerStyle={styles.contentContainer}
@@ -510,7 +523,6 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
<View style={[styles.avatarContainer, { flexDirection: 'row', flex: 1, gap: 10 }]}> <View style={[styles.avatarContainer, { flexDirection: 'row', flex: 1, gap: 10 }]}>
<View style={{}}> <View style={{}}>
<Image <Image
// source={{ uri: groupDetails?.imageBlob ? groupDetails?.imageBlob : selectedImage || "https://img.freepik.com/premium-vector/people-group-frame-icon_791764-2649.jpg" }}
source={{ uri: getProfileImage() }} source={{ uri: getProfileImage() }}
style={styles.avatar} style={styles.avatar}
onError={() => console.log("Error loading image")} onError={() => console.log("Error loading image")}
@@ -535,6 +547,7 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
], 'groupInfo')} ], 'groupInfo')}
{groupDetails.status === 'ACTIVE' ? {groupDetails.status === 'ACTIVE' ?
renderCard('Financial Rules', [ renderCard('Financial Rules', [
{ field: 'contributionAmount', label: 'Contribution', displayValue: () => `₹${groupDetails.contributionAmount.toLocaleString('en-IN')}` }, { field: 'contributionAmount', label: 'Contribution', displayValue: () => `₹${groupDetails.contributionAmount.toLocaleString('en-IN')}` },
@@ -551,12 +564,14 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
} }
{renderCard('Configuration', [ {renderCard('Configuration', [
{ field: 'cycleType', label: 'Payment Cycle', displayValue: () => groupDetails.cycleType }, { field: 'cycleType', label: 'Payment Cycle', displayValue: () => groupDetails.cycleType },
...(groupDetails.cycleType === 'CUSTOM' ? [{ field: 'customCycleDays' as keyof EditGroupPayload, 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 }, { field: 'visibility', label: 'Visibility', displayValue: () => groupDetails.visibility },
], 'configuration')} ], 'configuration')}
<View style={styles.card}> <View style={styles.card}>
<Text style={styles.cardTitle}>Participants ({groupDetails.totalParticipants})</Text> <Text style={styles.cardTitle}>Participants ({groupDetails.totalParticipants})</Text>
{groupDetails.participants.length > 0 ? ( {groupDetails.participants.length > 0 ? (
@@ -577,7 +592,6 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
</View> </View>
</ScrollView> </ScrollView>
{canStartGroup && ( {canStartGroup && (
<View style={styles.floatingButtonContainer}> <View style={styles.floatingButtonContainer}>
<TouchableOpacity <TouchableOpacity
@@ -593,7 +607,7 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
</TouchableOpacity> </TouchableOpacity>
</View> </View>
)} )}
{selectedUser?.cycleStatus !== "PENDING_PAYMENTS" && groupDetails.status === 'ACTIVE' && selectedUser?.cycleStatus === "PENDING_SELECTION" &&( {selectedUser?.cycleStatus !== "PENDING_PAYMENTS" && groupDetails.status === 'ACTIVE' && selectedUser?.cycleStatus === "PENDING_SELECTION" && (
<View style={styles.floatingButtonContainer}> <View style={styles.floatingButtonContainer}>
<TouchableOpacity <TouchableOpacity
style={styles.activateButton} style={styles.activateButton}
@@ -611,7 +625,7 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
} }
{ {
selectedUser?.selectedUser !== null && selectedUser?.cycleStatus === "READY_FOR_WITHDRAWAL" && selectedUser?.selectedUser !== null && selectedUser?.cycleStatus === "READY_FOR_WITHDRAWAL" &&
<View style={styles.floatingButtonContainer}> <View style={styles.floatingButtonContainer}>
<CustomButton <CustomButton
buttonBackground={colors?.pastelBlue} buttonBackground={colors?.pastelBlue}
@@ -630,6 +644,7 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#F3F4F6' }, container: { flex: 1, backgroundColor: '#F3F4F6' },
contentContainer: { padding: 16, paddingTop: 0, paddingBottom: 150 }, contentContainer: { padding: 16, paddingTop: 0, paddingBottom: 150 },
@@ -647,6 +662,7 @@ const styles = StyleSheet.create({
}, },
avatarContainer: { avatarContainer: {
position: "relative", position: "relative",
marginVertical: 16, marginVertical: 16,
@@ -682,8 +698,8 @@ const styles = StyleSheet.create({
marginBottom: 12, marginBottom: 12,
}, },
cardTitle: { fontSize: 18, fontWeight: 'bold', color: '#1F2937' }, cardTitle: { fontSize: 18, fontWeight: 'bold', color: '#1F2937' },
editButton: { padding: 8,backgroundColor:colors.pastelOrange, borderRadius: 15,elevation:5 }, editButton: { padding: 8, backgroundColor: colors.pastelOrange, borderRadius: 15, elevation: 5 },
editButtonText: { color: colors.black, fontSize: 15, fontWeight: 'bold' }, editButtonText: { color: colors.black, fontSize: 15, fontWeight: 'bold' },
fieldContainer: { borderBottomWidth: 1, borderBottomColor: '#F3F4F6', paddingBottom: 12, marginBottom: 12 }, fieldContainer: { borderBottomWidth: 1, borderBottomColor: '#F3F4F6', paddingBottom: 12, marginBottom: 12 },
infoLabel: { fontSize: 16, color: '#6B7280', marginBottom: 4 }, infoLabel: { fontSize: 16, color: '#6B7280', marginBottom: 4 },
infoValue: { fontSize: 16, fontWeight: '600', color: '#111827', textTransform: 'capitalize' }, infoValue: { fontSize: 16, fontWeight: '600', color: '#111827', textTransform: 'capitalize' },
@@ -731,4 +747,5 @@ const styles = StyleSheet.create({
}); });
export default EditGroupScreen; export default EditGroupScreen;
+4 -4
View File
@@ -63,7 +63,7 @@ const Payments: React.FC = () => {
// These are regular functions and do not call hooks inside them, which is correct. // These are regular functions and do not call hooks inside them, which is correct.
const getCycleData = async () => { const getCycleData = async () => {
try { try {
const response = await apiCall(`${API_BASE_URL}/groups/${groupId}/cycle-dashboard`, 'GET') as ApiResponse<any>; const response = await apiCall(`${BASE_URL}/payments/groups/${groupId}/cycle-dashboard`, 'GET') as ApiResponse<any>;
if (response && response.statusCode === 200) { if (response && response.statusCode === 200) {
setCycleData(response.data); setCycleData(response.data);
} }
@@ -131,7 +131,7 @@ const Payments: React.FC = () => {
} }
try { try {
const url = `${BASE_URL}groups/${groupId}/cycles/${paymentDetails.currentCycle}/waive/${currentParticipant.userId}?amountToWaive=${amountToWaive}`; const url = `https://api.godemo.in/FundCollect/api/payments/groups/${groupId}/cycles/${paymentDetails.currentCycle}/waive/${currentParticipant.userId}?amountToWaive=${amountToWaive}`;
const response = await apiCall(url, 'PUT', {}) as any; const response = await apiCall(url, 'PUT', {}) as any;
if (response && response.statusCode === 200) { if (response && response.statusCode === 200) {
@@ -248,8 +248,8 @@ const Payments: React.FC = () => {
<Text style={styles.summaryValue}>₹{paymentDetails.contributionAmount}</Text> <Text style={styles.summaryValue}>₹{paymentDetails.contributionAmount}</Text>
</View> </View>
<View style={styles.summaryBox}> <View style={styles.summaryBox}>
<Text style={styles.summaryLabel}>Total Due</Text> <Text style={styles.summaryLabel}>Total Amount</Text>
<Text style={[styles.summaryValue, { color: colors.danger }]}>₹{paymentDetails.totalDueWithLateFee}</Text> <Text style={[styles.summaryValue, { color: colors.black }]}>₹{paymentDetails.totalDueWithLateFee}</Text>
</View> </View>
</View> </View>
<Text style={styles.sectionTitle}>Participants</Text> <Text style={styles.sectionTitle}>Participants</Text>
@@ -306,16 +306,9 @@ const styles = StyleSheet.create({
button: { button: {
paddingVertical: 16, paddingVertical: 16,
paddingHorizontal: 50, paddingHorizontal: 50,
backgroundColor: '#4f46e5', backgroundColor: colors.pastelOrange,
borderRadius: 30, borderRadius: 30,
elevation: 3, elevation: 5,
shadowColor: "#4f46e5",
shadowOffset: {
width: 0,
height: 4,
},
shadowOpacity: 0.3,
shadowRadius: 4.65,
}, },
buttonDisabled: { buttonDisabled: {
backgroundColor: '#9ca3af', backgroundColor: '#9ca3af',
@@ -323,7 +316,7 @@ const styles = StyleSheet.create({
}, },
buttonText: { buttonText: {
fontSize: 18, fontSize: 18,
color: '#fff', color: colors.black,
fontWeight: 'bold', fontWeight: 'bold',
}, },
winnerTextContainer: { winnerTextContainer: {
+2
View File
@@ -4,6 +4,7 @@ import { Ionicons } from '@react-native-vector-icons/ionicons'
import CustomModal from '../../components/common/CustomModal' import CustomModal from '../../components/common/CustomModal'
import { apiCall } from '../../utils/ApiHelper' import { apiCall } from '../../utils/ApiHelper'
import { BASE_URL } from '../../utils/ApiUrls' import { BASE_URL } from '../../utils/ApiUrls'
import colors from '../../components/colors'
interface WinnerUserProps { interface WinnerUserProps {
data: { data: {
@@ -111,6 +112,7 @@ const WinnerUser: React.FC<WinnerUserProps> = ({ data, visible, onClose, groupId
onClose={onClose} onClose={onClose}
confirmText={canProceedToWithdrawal ? 'Proceed to Withdraw' : 'Close'} confirmText={canProceedToWithdrawal ? 'Proceed to Withdraw' : 'Close'}
onConfirm={canProceedToWithdrawal ? onConfirm : onClose} onConfirm={canProceedToWithdrawal ? onConfirm : onClose}
confirmButton={colors.pastelOrange}
> >
<View style={styles.container}> <View style={styles.container}>
<Animated.View style={{ transform: [{ scale: scaleAnim }] }}> <Animated.View style={{ transform: [{ scale: scaleAnim }] }}>
+2 -2
View File
@@ -375,7 +375,7 @@ const Profile = () => {
<View style={styles.header}> <View style={styles.header}>
<Text style={styles.headerTitle}>User Profile</Text> <Text style={styles.headerTitle}>User Profile</Text>
<TouchableOpacity onPress={() => confirmLogout(navigation)} style={styles.refreshButton}> <TouchableOpacity onPress={() => confirmLogout(navigation)} style={styles.refreshButton}>
<Ionicons name="log-out-outline" size={24} color={colors.primary} /> <Ionicons name="log-out-outline" size={24} color={colors.black} />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
{/* if (fetching || !user) { {/* if (fetching || !user) {
@@ -696,7 +696,7 @@ const styles = StyleSheet.create({
borderBottomRightRadius: 20, borderBottomRightRadius: 20,
}, },
headerTitle: { headerTitle: {
fontSize: 22, fontWeight: "600", color: colors.primary fontSize: 22, fontWeight: "600", color: colors.black
}, },
refreshButton: { refreshButton: {
padding: 4, padding: 4,
+1 -4
View File
@@ -176,10 +176,7 @@ export const getFcmToken = async (userId: string) => {
console.log('FCM Token:', token); console.log('FCM Token:', token);
// Send token to backend with user ID // Send token to backend with user ID
await apiCall(STORE_FCM_TOKEN, 'POST', { await apiCall(`${STORE_FCM_TOKEN}${token}`, 'POST');
token,
userId
});
console.log('FCM Token sent to backend', token); console.log('FCM Token sent to backend', token);
return token; return token;