815 lines
33 KiB
TypeScript
815 lines
33 KiB
TypeScript
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
|
import {
|
|
View,
|
|
Text,
|
|
StyleSheet,
|
|
TouchableOpacity,
|
|
Image,
|
|
ActivityIndicator,
|
|
ToastAndroid,
|
|
FlatList,
|
|
Modal,
|
|
Platform,
|
|
TouchableWithoutFeedback,
|
|
Keyboard,
|
|
TextInput,
|
|
ScrollView,
|
|
Dimensions,
|
|
KeyboardAvoidingView,
|
|
Share,
|
|
RefreshControl,
|
|
} from 'react-native';
|
|
import { useRoute, useNavigation } from '@react-navigation/native';
|
|
import { Picker } from '@react-native-picker/picker';
|
|
import Clipboard from '@react-native-clipboard/clipboard';
|
|
import { launchImageLibrary } from 'react-native-image-picker';
|
|
import * as Keychain from 'react-native-keychain';
|
|
import LucideIcon from '../../components/common/LucideIcon';
|
|
import { responsiveFontSize } from 'react-native-responsive-dimensions';
|
|
|
|
import AppView from '../../components/AppView';
|
|
import { useTheme } from '../../hooks/ui/useTheme';
|
|
import { useFetch } from '../../hooks/core/useFetch';
|
|
import { useUpdate } from '../../hooks/core/useUpdate';
|
|
import { useInvite } from '../../hooks/core/useInvite';
|
|
import { BASE_URL, UPLOAD_GROUP_IMAGE } from '../../utils/ApiUrls';
|
|
import FontsFamily from '../../constents/FontsFamily';
|
|
import SkeletonLoader from '../../components/common/SkeletonLoader';
|
|
import WinnerUser from './WinnerUser';
|
|
|
|
const { width } = Dimensions.get('window');
|
|
|
|
// --- TYPE DEFINITIONS ---
|
|
interface Participant {
|
|
id: string;
|
|
firstName: string;
|
|
lastName: string | null;
|
|
mobile: string;
|
|
role: 'ORGANIZER' | 'USER';
|
|
}
|
|
|
|
export interface EditGroupPayload {
|
|
groupName: string;
|
|
description: string;
|
|
maxParticipants: number;
|
|
minParticipants: number;
|
|
lateFeeMode: 'DAILY' | 'FIXED' | string;
|
|
lateFeeValue: number;
|
|
latePaymentCharge: number;
|
|
visibility: 'PUBLIC' | 'PRIVATE';
|
|
contributionAmount: number;
|
|
cycleType: 'QUARTERLY' | 'MONTHLY' | 'WEEKLY' | 'CUSTOM' | 'HALF_YEARLY' | 'YEARLY';
|
|
entryFee: number;
|
|
customCycleDays: number | null;
|
|
}
|
|
|
|
type EditSection = 'groupInfo' | 'financialRules' | 'configuration' | null;
|
|
|
|
const EditGroupScreen: React.FC = () => {
|
|
const theme = useTheme();
|
|
const styles = React.useMemo(() => getStyles(theme), [theme]);
|
|
const route = useRoute<any>();
|
|
const navigation = useNavigation<any>();
|
|
const { groupId } = route?.params || {};
|
|
|
|
// State
|
|
const [activeSection, setActiveSection] = useState<EditSection>(null);
|
|
const [editableData, setEditableData] = useState<EditGroupPayload | null>(null);
|
|
const [originalData, setOriginalData] = useState<EditGroupPayload | null>(null);
|
|
const [changedFields, setChangedFields] = useState<Set<keyof EditGroupPayload>>(new Set());
|
|
const [selectedImage, setSelectedImage] = useState<string | null>(null);
|
|
const [imageUpdating, setImageUpdating] = useState(false);
|
|
const [selectedUserModal, setSelectedUserModel] = useState(false);
|
|
const [isActivating, setIsActivating] = useState(false);
|
|
const [showInviteTooltip, setShowInviteTooltip] = useState(false);
|
|
|
|
// --- DATA FETCHING ---
|
|
const {
|
|
data: groupDetailsRes,
|
|
isLoading: isGroupLoading,
|
|
isRefetching: isRefetchingGroup,
|
|
refetch: refetchGroup,
|
|
} = useFetch<any>(
|
|
`${BASE_URL}fund/group/details?groupId=${groupId}`,
|
|
undefined,
|
|
{ enabled: !!groupId }
|
|
);
|
|
|
|
const groupDetails = groupDetailsRes?.data;
|
|
|
|
// Initialize data
|
|
useEffect(() => {
|
|
if (groupDetails) {
|
|
const newData: EditGroupPayload = {
|
|
groupName: groupDetails.groupName || '',
|
|
description: groupDetails.description || '',
|
|
maxParticipants: groupDetails.maxParticipants || 0,
|
|
minParticipants: groupDetails.minParticipants || 0,
|
|
contributionAmount: groupDetails.contributionAmount || 0,
|
|
customCycleDays: groupDetails.customCycleDays || 0,
|
|
cycleType: groupDetails.cycleType || 'MONTHLY',
|
|
latePaymentCharge: groupDetails.latePaymentCharge ?? 0,
|
|
visibility: groupDetails.visibility || 'PUBLIC',
|
|
lateFeeMode: groupDetails.lateFeeMode ?? null,
|
|
lateFeeValue: groupDetails.lateFeeValue ?? 0,
|
|
entryFee: groupDetails.entryFee ?? 0,
|
|
};
|
|
setOriginalData(newData);
|
|
setEditableData(newData);
|
|
setChangedFields(new Set());
|
|
|
|
if (groupDetails.status === 'APPROVED') {
|
|
setShowInviteTooltip(true);
|
|
const timer = setTimeout(() => setShowInviteTooltip(false), 5000);
|
|
return () => clearTimeout(timer);
|
|
}
|
|
}
|
|
|
|
// DEBUG: Log Token
|
|
const logToken = async () => {
|
|
try {
|
|
const credentials = await Keychain.getGenericPassword();
|
|
if (credentials) {
|
|
console.log("🔐 AUTH TOKEN:", credentials.password);
|
|
} else {
|
|
console.log("🔐 No credentials stored");
|
|
}
|
|
} catch (error) {
|
|
console.error("Error retrieving token:", error);
|
|
}
|
|
};
|
|
logToken();
|
|
|
|
}, [groupDetails]);
|
|
|
|
const groupStatusFromDetails = groupDetails?.status;
|
|
const { data: paymentStatusRes, refetch: refetchPaymentStatus } = useFetch<any>(
|
|
`${BASE_URL}payments/group/${groupId}/payment/paid`,
|
|
undefined,
|
|
{ enabled: !!groupId && (groupStatusFromDetails === 'ACTIVE' || groupStatusFromDetails === 'DELAY') }
|
|
);
|
|
|
|
const paymentStatus = paymentStatusRes?.data;
|
|
|
|
const { mutate: generateInvite, isPending: isInviting } = useInvite({
|
|
onSuccess: (res) => {
|
|
if (res.data) {
|
|
Clipboard.setString(res.data);
|
|
ToastAndroid.show('Invite link copied', ToastAndroid.SHORT);
|
|
Share.share({ message: `Join my group: ${res.data}` });
|
|
}
|
|
},
|
|
onError: (error: any) => {
|
|
ToastAndroid.show(error?.message || 'Failed to generate invite', ToastAndroid.SHORT);
|
|
}
|
|
});
|
|
|
|
// --- MUTATIONS ---
|
|
const { mutate: updateGroup, isPending: isSaving } = useUpdate<any, any>(
|
|
`${BASE_URL}fund/groups/${groupId}/edit`,
|
|
{
|
|
onSuccess: () => {
|
|
ToastAndroid.show('Group updated successfully', ToastAndroid.SHORT);
|
|
setActiveSection(null);
|
|
refetchGroup();
|
|
},
|
|
onError: (err: any) => {
|
|
ToastAndroid.show(err?.message || 'Failed to update group', ToastAndroid.SHORT);
|
|
},
|
|
}
|
|
);
|
|
|
|
const activateGroup = async () => {
|
|
setIsActivating(true);
|
|
try {
|
|
const credentials = await Keychain.getGenericPassword();
|
|
const token = credentials ? credentials.password : '';
|
|
const response = await fetch(`${BASE_URL}fund/group/start?groupId=${groupId}`, {
|
|
method: 'GET',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
});
|
|
|
|
if (response.ok) {
|
|
ToastAndroid.show('Group activated successfully', ToastAndroid.SHORT);
|
|
refetchGroup();
|
|
} else {
|
|
const errorData = await response.json().catch(() => ({}));
|
|
throw new Error(errorData.message || 'Activation failed');
|
|
}
|
|
} catch (error: any) {
|
|
ToastAndroid.show(error?.message || 'Activation failed', ToastAndroid.SHORT);
|
|
} finally {
|
|
setIsActivating(false);
|
|
}
|
|
};
|
|
|
|
// --- HANDLERS ---
|
|
const handleSave = () => {
|
|
if (!editableData || !groupDetails) return;
|
|
|
|
if (editableData.maxParticipants < groupDetails.totalParticipants) {
|
|
ToastAndroid.show('Capacity cannot be less than current participants', ToastAndroid.SHORT);
|
|
return;
|
|
}
|
|
|
|
// Construct payload strictly matching user requirements
|
|
const payloadData: EditGroupPayload = {
|
|
groupName: editableData.groupName,
|
|
description: editableData.description,
|
|
maxParticipants: Number(editableData.maxParticipants || 0),
|
|
minParticipants: Number(editableData.minParticipants || 0),
|
|
lateFeeMode: editableData.lateFeeMode || 'DAILY',
|
|
lateFeeValue: Number(editableData.lateFeeValue || 0),
|
|
latePaymentCharge: Number(editableData.latePaymentCharge || 0),
|
|
visibility: editableData.visibility || 'PUBLIC',
|
|
contributionAmount: Number(editableData.contributionAmount || 0),
|
|
cycleType: editableData.cycleType || 'MONTHLY',
|
|
entryFee: Number(editableData.entryFee || 0),
|
|
customCycleDays: editableData.cycleType === 'CUSTOM' ? Number(editableData.customCycleDays || 0) : null,
|
|
};
|
|
|
|
updateGroup(payloadData);
|
|
};
|
|
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
const onRefresh = useCallback(() => {
|
|
setRefreshing(true);
|
|
const promises = [refetchGroup()];
|
|
if (groupStatusFromDetails === 'ACTIVE') {
|
|
promises.push(refetchPaymentStatus());
|
|
}
|
|
Promise.all(promises).finally(() => setRefreshing(false));
|
|
}, [refetchGroup, refetchPaymentStatus, groupStatusFromDetails]);
|
|
|
|
const handleUpdateImage = async () => {
|
|
if (!selectedImage) return;
|
|
setImageUpdating(true);
|
|
try {
|
|
const credentials = await Keychain.getGenericPassword();
|
|
const token = credentials ? credentials.password : '';
|
|
const fileExtension = selectedImage.split('.').pop();
|
|
const formData = new FormData();
|
|
formData.append('file', {
|
|
uri: selectedImage,
|
|
type: 'image/jpeg',
|
|
name: `group_${groupId}.${fileExtension || 'jpg'}`,
|
|
} as any);
|
|
|
|
const response = await fetch(`${UPLOAD_GROUP_IMAGE}${groupId}/upload-image`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'multipart/form-data',
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
body: formData,
|
|
});
|
|
|
|
if (response.status === 200) {
|
|
ToastAndroid.show('Image updated', ToastAndroid.SHORT);
|
|
refetchGroup();
|
|
setSelectedImage(null);
|
|
} else {
|
|
throw new Error('Upload failed');
|
|
}
|
|
} catch (e) {
|
|
ToastAndroid.show('Failed to upload image', ToastAndroid.SHORT);
|
|
} finally {
|
|
setImageUpdating(false);
|
|
}
|
|
};
|
|
|
|
const handleInvite = () => {
|
|
generateInvite(groupId);
|
|
};
|
|
|
|
const pickImage = () => {
|
|
launchImageLibrary({ mediaType: 'photo', quality: 0.5 }, (res) => {
|
|
if (res.assets?.[0]?.uri) setSelectedImage(res.assets[0].uri);
|
|
});
|
|
};
|
|
|
|
const handleDistribute = () => {
|
|
navigation.navigate('RandomSelectUserScreen', { groupId, cycleNumber: paymentStatus?.currentCycle || 0 });
|
|
};
|
|
|
|
const handleFieldChange = (field: keyof EditGroupPayload, value: any) => {
|
|
if (!editableData) return;
|
|
const newData = { ...editableData, [field]: value };
|
|
setEditableData(newData);
|
|
|
|
const newChangedFields = new Set(changedFields);
|
|
if (originalData && originalData[field] !== value) {
|
|
newChangedFields.add(field);
|
|
} else {
|
|
newChangedFields.delete(field);
|
|
}
|
|
setChangedFields(newChangedFields);
|
|
};
|
|
|
|
// --- RENDER HELPERS ---
|
|
const renderField = (field: keyof EditGroupPayload, label: string) => {
|
|
if (!editableData) return null;
|
|
|
|
const isNum = ['contributionAmount', 'latePaymentCharge', 'maxParticipants', 'customCycleDays', 'minParticipants', 'entryFee', 'lateFeeValue'].includes(field);
|
|
|
|
return (
|
|
<View style={styles.modalFieldContainer}>
|
|
<Text style={styles.modalLabel}>{label}</Text>
|
|
{field === 'cycleType' || field === 'visibility' || field === 'lateFeeMode' ? (
|
|
<View style={styles.chipContainer}>
|
|
{(field === 'cycleType'
|
|
? ['MONTHLY', 'QUARTERLY', 'HALF_YEARLY', 'YEARLY', 'CUSTOM']
|
|
: field === 'lateFeeMode'
|
|
? ['DAILY', 'MONTHLY']
|
|
: ['PUBLIC', 'PRIVATE']
|
|
).map((option) => (
|
|
<TouchableOpacity
|
|
key={option}
|
|
style={[
|
|
styles.chip,
|
|
editableData[field] === option && styles.activeChip,
|
|
]}
|
|
onPress={() => handleFieldChange(field, option)}
|
|
>
|
|
<Text
|
|
style={[
|
|
styles.chipText,
|
|
editableData[field] === option && styles.activeChipText,
|
|
]}
|
|
>
|
|
{option}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
))}
|
|
</View>
|
|
) : (
|
|
<TextInput
|
|
style={styles.modalInput}
|
|
value={String(editableData[field] ?? '')}
|
|
placeholder={`Enter ${label}`}
|
|
placeholderTextColor="#999"
|
|
onChangeText={(text) => handleFieldChange(field, isNum ? Number(text.replace(/[^0-9.]/g, '')) : text)}
|
|
keyboardType={isNum ? 'decimal-pad' : 'default'}
|
|
multiline={field === 'description'}
|
|
numberOfLines={field === 'description' ? 3 : 1}
|
|
textAlignVertical={field === 'description' ? 'top' : 'center'}
|
|
/>
|
|
)}
|
|
</View>
|
|
);
|
|
};
|
|
|
|
// --- LIST SECTIONS ---
|
|
const SECTIONS = [
|
|
{ id: 'cover', type: 'cover' },
|
|
{ id: 'stats', type: 'stats' },
|
|
{ id: 'info', type: 'card', title: 'Group Info', section: 'groupInfo' as EditSection },
|
|
{ id: 'financial', type: 'card', title: 'Financial Rules', section: 'financialRules' as EditSection },
|
|
{ id: 'config', type: 'card', title: 'Configuration', section: 'configuration' as EditSection },
|
|
{ id: 'participants', type: 'participants' },
|
|
{ id: 'actions', type: 'actions' },
|
|
];
|
|
|
|
const renderItem = ({ item }: { item: any }) => {
|
|
if (!groupDetails) return null;
|
|
|
|
if (item.type === 'cover') {
|
|
const coverImageUri = selectedImage || (groupDetails.imageUrl ? groupDetails.imageUrl : `https://api.dicebear.com/7.x/shapes/png?seed=${encodeURIComponent(groupDetails.groupName)}`);
|
|
|
|
return (
|
|
<View style={styles.coverSection}>
|
|
<Image source={{ uri: coverImageUri }} style={styles.coverImage} />
|
|
<View style={styles.coverOverlay} />
|
|
|
|
<View style={styles.coverHeader}>
|
|
<TouchableOpacity onPress={() => navigation.goBack()} style={styles.iconBtn}>
|
|
<LucideIcon name="arrow-back" size={24} color="#fff" />
|
|
</TouchableOpacity>
|
|
<Text style={styles.headerTitle}>Manage Group</Text>
|
|
{groupDetails.status === 'APPROVED' ? (
|
|
<View style={styles.inviteContainer}>
|
|
<TouchableOpacity onPress={handleInvite} style={styles.iconBtn}>
|
|
<LucideIcon name="person-add-outline" size={22} color="#ffffffff" />
|
|
</TouchableOpacity>
|
|
|
|
{showInviteTooltip && (
|
|
<TouchableOpacity
|
|
activeOpacity={0.9}
|
|
onPress={() => setShowInviteTooltip(false)}
|
|
style={styles.inviteTooltip}
|
|
>
|
|
<Text style={styles.tooltipText}>Invite members</Text>
|
|
<View style={styles.tooltipArrow} />
|
|
</TouchableOpacity>
|
|
)}
|
|
</View>
|
|
) : (
|
|
<View style={{ width: 40 }} />
|
|
)}
|
|
</View>
|
|
|
|
<View style={styles.coverContent}>
|
|
<Text style={styles.groupNameTitle}>{groupDetails.groupName}</Text>
|
|
{groupDetails.description && (
|
|
<View style={styles.descContainer}>
|
|
<Text style={styles.descText} numberOfLines={2}>{groupDetails.description}</Text>
|
|
</View>
|
|
)}
|
|
<View style={styles.badgesRow}>
|
|
<View style={styles.badge}><Text style={styles.badgeText}>{groupDetails.status}</Text></View>
|
|
<View style={styles.badge}><Text style={styles.badgeText}>{groupDetails.visibility}</Text></View>
|
|
</View>
|
|
</View>
|
|
|
|
<TouchableOpacity onPress={pickImage} style={styles.cameraBtn}>
|
|
<LucideIcon name="camera" size={20} color={theme.colors.primary} />
|
|
</TouchableOpacity>
|
|
|
|
{selectedImage && (
|
|
<View style={styles.uploadFloatWrapper}>
|
|
<View style={[styles.uploadTooltip, { backgroundColor: theme.colors.primary }]}>
|
|
<Text style={[styles.uploadTooltipText, { color: theme.colors.onPrimary }]}>Upload Image</Text>
|
|
<View style={[styles.uploadTooltipArrow, { borderTopColor: theme.colors.primary }]} />
|
|
</View>
|
|
<TouchableOpacity onPress={handleUpdateImage} style={[styles.uploadFloatBtnInner, { backgroundColor: theme.colors.primary }]} disabled={imageUpdating}>
|
|
{imageUpdating ? <ActivityIndicator color={theme.colors.onPrimary} /> : <LucideIcon name="cloud-upload" size={24} color={theme.colors.onPrimary} />}
|
|
</TouchableOpacity>
|
|
</View>
|
|
)}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
if (item.type === 'stats') {
|
|
return (
|
|
<View style={styles.statsContainer}>
|
|
<View style={styles.statBox}>
|
|
<Text style={styles.statValue}>{groupDetails.totalParticipants}</Text>
|
|
<Text style={styles.statLabel}>Participants</Text>
|
|
</View>
|
|
<View style={styles.statDivider} />
|
|
<View style={styles.statBox}>
|
|
<Text style={styles.statValue}>₹{groupDetails.contributionAmount}</Text>
|
|
<Text style={styles.statLabel}>Contribution</Text>
|
|
</View>
|
|
<View style={styles.statDivider} />
|
|
<View style={styles.statBox}>
|
|
<Text style={styles.statValue}>{groupDetails.cycleType}</Text>
|
|
<Text style={styles.statLabel}>Cycle</Text>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
if (item.type === 'card') {
|
|
const isEditable = groupDetails.status === 'PENDING' || groupDetails.status === 'APPROVED';
|
|
const fields = item.section === 'groupInfo'
|
|
? ['groupName', 'description', 'visibility']
|
|
: item.section === 'financialRules'
|
|
? ['contributionAmount', 'entryFee', 'latePaymentCharge', 'lateFeeMode']
|
|
: ['maxParticipants', 'minParticipants', 'cycleType', ...(groupDetails.customCycleDays ? ['customCycleDays'] : [])];
|
|
|
|
const getDisplayValue = (field: string) => {
|
|
const val = groupDetails[field as keyof typeof groupDetails];
|
|
if (field.includes('Amount') || field.includes('Fee') || field.includes('Charge') || field.includes('Value')) return `₹${val ?? 0}`;
|
|
return String(val ?? '-');
|
|
};
|
|
|
|
return (
|
|
<TouchableOpacity
|
|
style={styles.cardContainer}
|
|
onPress={() => isEditable && setActiveSection(item.section)}
|
|
activeOpacity={isEditable ? 0.7 : 1}
|
|
>
|
|
<View style={styles.cardHeader}>
|
|
<Text style={styles.cardTitle}>{item.title}</Text>
|
|
{isEditable && <LucideIcon name="create-outline" size={20} color={theme.colors.primary} />}
|
|
</View>
|
|
<View style={styles.cardBody}>
|
|
{fields.map((f: string) => (
|
|
<Text key={f} style={styles.cardFieldText}>
|
|
<Text style={{ color: '#888' }}>{f.replace(/([A-Z])/g, ' $1').trim()}: </Text>
|
|
{getDisplayValue(f)}
|
|
</Text>
|
|
))}
|
|
</View>
|
|
</TouchableOpacity>
|
|
);
|
|
}
|
|
|
|
if (item.type === 'participants') {
|
|
return (
|
|
<View style={styles.cardContainer}>
|
|
<Text style={styles.cardTitle}>Participants ({groupDetails.participants.length})</Text>
|
|
{groupDetails.participants.slice(0, 3).map((p: any) => (
|
|
<View key={p.id} style={styles.participantRow}>
|
|
<View style={styles.avatarPlaceholder}><Text style={{ color: '#fff' }}>{p.firstName[0]}</Text></View>
|
|
<View style={{ flex: 1 }}>
|
|
<Text style={styles.pName}>{p.firstName} {p.lastName}</Text>
|
|
<Text style={styles.pRole}>{p.role}</Text>
|
|
</View>
|
|
</View>
|
|
))}
|
|
{groupDetails.participants.length > 3 && (
|
|
<TouchableOpacity style={styles.viewAllBtn}>
|
|
<Text style={styles.viewAllText}>View All Participants</Text>
|
|
</TouchableOpacity>
|
|
)}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
if (item.type === 'actions') {
|
|
const isApproved = groupDetails.status === 'APPROVED';
|
|
const notEnoughParticipants = groupDetails.totalParticipants < groupDetails.minParticipants;
|
|
|
|
return (
|
|
<View style={styles.actionsContainer}>
|
|
{isApproved && notEnoughParticipants && (
|
|
<View style={styles.hintContainer}>
|
|
<LucideIcon name="information-circle-outline" size={20} color="#F59E0B" />
|
|
<Text style={styles.hintText}>
|
|
To start the group, a minimum of {groupDetails.minParticipants} participants are needed.
|
|
</Text>
|
|
</View>
|
|
)}
|
|
|
|
{paymentStatus?.cycleStatus === 'PENDING_SELECTION' && (
|
|
<TouchableOpacity onPress={handleDistribute} style={styles.primaryBtn}>
|
|
<Text style={styles.btnText}>Distribute Funds</Text>
|
|
</TouchableOpacity>
|
|
)}
|
|
{paymentStatus?.cycleStatus === 'READY_FOR_WITHDRAWAL' && paymentStatus?.selectedUser && (
|
|
<TouchableOpacity onPress={() => setSelectedUserModel(true)} style={styles.secondaryBtn}>
|
|
<Text style={styles.secondaryBtnText}>View Winner</Text>
|
|
</TouchableOpacity>
|
|
)}
|
|
</View>
|
|
);
|
|
}
|
|
return null;
|
|
};
|
|
|
|
if (isGroupLoading) {
|
|
return (
|
|
<AppView style={{ backgroundColor: theme.colors.background }}>
|
|
<View style={{ padding: 20, paddingTop: 60 }}>
|
|
<SkeletonLoader width="100%" height={200} style={{ borderRadius: 16, marginBottom: 20 }} />
|
|
<SkeletonLoader width="100%" height={100} style={{ borderRadius: 16, marginBottom: 20 }} />
|
|
<SkeletonLoader width="100%" height={150} style={{ borderRadius: 16, marginBottom: 20 }} />
|
|
</View>
|
|
</AppView>
|
|
);
|
|
}
|
|
|
|
if (!groupDetails) return null;
|
|
|
|
return (
|
|
<AppView paddingHorizontal={0} style={{ backgroundColor: theme.colors.background, flex: 1 }}>
|
|
<FlatList
|
|
data={SECTIONS}
|
|
renderItem={renderItem}
|
|
keyExtractor={item => item.id}
|
|
contentContainerStyle={{ paddingBottom: 150 }}
|
|
showsVerticalScrollIndicator={false}
|
|
refreshControl={
|
|
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} colors={[theme.colors.primary]} />
|
|
}
|
|
/>
|
|
|
|
{/* Floating Activate Button */}
|
|
{groupDetails.status === 'APPROVED' && groupDetails.totalParticipants >= groupDetails.minParticipants && (
|
|
<View style={styles.floatingBtnContainer}>
|
|
<TouchableOpacity
|
|
onPress={() => activateGroup()}
|
|
style={styles.floatingActivateBtn}
|
|
disabled={isActivating}
|
|
>
|
|
{isActivating ? (
|
|
<ActivityIndicator color="#fff" />
|
|
) : (
|
|
<>
|
|
<LucideIcon name="power" size={24} color="#fff" style={{ marginRight: 8 }} />
|
|
<Text style={styles.floatingBtnText}>Activate Group</Text>
|
|
<LucideIcon name="arrow-forward" size={20} color="#fff" style={{ marginLeft: 8, opacity: 0.8 }} />
|
|
</>
|
|
)}
|
|
</TouchableOpacity>
|
|
</View>
|
|
)}
|
|
|
|
{/* --- EDIT MODAL --- */}
|
|
<Modal
|
|
visible={activeSection !== null}
|
|
animationType="slide"
|
|
transparent={true}
|
|
onRequestClose={() => setActiveSection(null)}
|
|
>
|
|
<View style={styles.modalOverlay}>
|
|
<View style={styles.modalContainer}>
|
|
<View style={styles.modalHeader}>
|
|
<Text style={styles.modalTitle}>
|
|
{activeSection === 'groupInfo' ? 'Group Information' :
|
|
activeSection === 'financialRules' ? 'Financial Rules' : 'Configuration'}
|
|
</Text>
|
|
<TouchableOpacity onPress={() => setActiveSection(null)} style={styles.closeBtn}>
|
|
<LucideIcon name="close" size={24} color="#333" />
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : undefined} style={{ flex: 1 }}>
|
|
<ScrollView style={styles.modalContent} showsVerticalScrollIndicator={false} contentContainerStyle={{ paddingBottom: 20 }}>
|
|
{activeSection === 'groupInfo' && (
|
|
<>
|
|
{renderField('groupName', 'Group Name')}
|
|
{renderField('description', 'Description')}
|
|
{renderField('visibility', 'Visibility')}
|
|
</>
|
|
)}
|
|
{activeSection === 'financialRules' && (
|
|
<>
|
|
{renderField('contributionAmount', 'Contribution Amount')}
|
|
{renderField('entryFee', 'Entry Fee')}
|
|
{renderField('latePaymentCharge', 'Late Payment Charge')}
|
|
{renderField('lateFeeMode', 'Late Fee Mode')}
|
|
</>
|
|
)}
|
|
{activeSection === 'configuration' && (
|
|
<>
|
|
{renderField('maxParticipants', 'Max Participants')}
|
|
{renderField('minParticipants', 'Min Participants')}
|
|
{renderField('cycleType', 'Payment Cycle')}
|
|
{editableData?.cycleType === 'CUSTOM' && renderField('customCycleDays', 'Cycle Days')}
|
|
</>
|
|
)}
|
|
</ScrollView>
|
|
</KeyboardAvoidingView>
|
|
|
|
<View style={styles.modalFooter}>
|
|
<TouchableOpacity onPress={handleSave} style={styles.saveBtn} disabled={isSaving}>
|
|
{isSaving ? <ActivityIndicator color="#fff" /> : <Text style={styles.saveBtnText}>Save Changes</Text>}
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
</Modal>
|
|
|
|
{selectedUserModal && (
|
|
<WinnerUser
|
|
groupId={groupId}
|
|
data={paymentStatus}
|
|
visible={selectedUserModal}
|
|
onClose={() => setSelectedUserModel(false)}
|
|
/>
|
|
)}
|
|
</AppView>
|
|
);
|
|
};
|
|
|
|
const getStyles = (theme: any) => StyleSheet.create({
|
|
// Cover
|
|
coverSection: { height: 280, position: 'relative' },
|
|
coverImage: { width: '100%', height: '100%', resizeMode: 'cover' },
|
|
coverOverlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0,0,0,0.4)' },
|
|
coverHeader: { position: 'absolute', top: 30, left: 20, right: 20, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
|
|
iconBtn: { padding: 8, backgroundColor: 'rgba(255,255,255,0.2)', borderRadius: 20 },
|
|
headerTitle: { color: '#fff', fontSize: 18, fontFamily: FontsFamily.nunitoBold },
|
|
coverContent: { position: 'absolute', bottom: 30, left: 20, right: 20 },
|
|
groupNameTitle: { color: '#fff', fontSize: 24, fontFamily: FontsFamily.nunitoBold, marginBottom: 8 },
|
|
descContainer: { backgroundColor: 'rgba(255,255,255,0.15)', padding: 10, borderRadius: 8, borderLeftWidth: 3, borderLeftColor: '#fff', marginBottom: 12 },
|
|
descText: { color: '#eee', fontSize: 14, fontFamily: FontsFamily.nunitoRegular },
|
|
badgesRow: { flexDirection: 'row', gap: 10 },
|
|
badge: { backgroundColor: 'rgba(0,0,0,0.5)', paddingHorizontal: 10, paddingVertical: 4, borderRadius: 12 },
|
|
badgeText: { color: '#fff', fontSize: 12, fontWeight: 'bold' },
|
|
cameraBtn: { position: 'absolute', bottom: -20, right: 30, width: 44, height: 44, borderRadius: 22, backgroundColor: '#fff', elevation: 4, justifyContent: 'center', alignItems: 'center' },
|
|
uploadFloatBtn: { position: 'absolute', bottom: 40, right: 30, width: 50, height: 50, borderRadius: 25, elevation: 5, justifyContent: 'center', alignItems: 'center' },
|
|
uploadFloatWrapper: { position: 'absolute', bottom: 40, right: 30, alignItems: 'center' },
|
|
uploadTooltip: { position: 'absolute', bottom: 58, right: 0, paddingHorizontal: 10, paddingVertical: 5, borderRadius: 8, elevation: 6, zIndex: 10, minWidth: 100, alignItems: 'center' },
|
|
uploadTooltipText: { fontSize: 12, fontFamily: FontsFamily.nunitoBold },
|
|
uploadTooltipArrow: { position: 'absolute', bottom: -5, width: 0, height: 0, borderLeftWidth: 6, borderRightWidth: 6, borderTopWidth: 6, borderLeftColor: 'transparent', borderRightColor: 'transparent' },
|
|
uploadFloatBtnInner: { width: 50, height: 50, borderRadius: 25, elevation: 5, justifyContent: 'center', alignItems: 'center' },
|
|
|
|
// Stats
|
|
statsContainer: { flexDirection: 'row', backgroundColor: theme.colors.card, margin: 20, marginTop: 30, borderRadius: 16, padding: 20, elevation: 2, justifyContent: 'space-between', alignItems: 'center' },
|
|
statBox: { alignItems: 'center', flex: 1 },
|
|
statValue: { fontSize: 18, fontFamily: FontsFamily.nunitoBold, color: theme.colors.textPrimary },
|
|
statLabel: { fontSize: 12, fontFamily: FontsFamily.nunitoRegular, color: theme.colors.textSecondary },
|
|
statDivider: { width: 1, height: 30, backgroundColor: '#eee' },
|
|
|
|
// Cards
|
|
cardContainer: { backgroundColor: theme.colors.card, marginHorizontal: 20, marginBottom: 16, borderRadius: 16, padding: 16, elevation: 2 },
|
|
cardHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12, paddingBottom: 10, borderBottomWidth: 1, borderBottomColor: '#f0f0f0' },
|
|
cardTitle: { fontSize: 16, fontFamily: FontsFamily.nunitoBold, color: theme.colors.textPrimary },
|
|
cardBody: {},
|
|
cardFieldText: { fontSize: 14, color: theme.colors.textPrimary, marginBottom: 6, fontFamily: FontsFamily.nunitoMedium },
|
|
moreText: { fontSize: 12, color: theme.colors.textSecondary, marginTop: 4, fontStyle: 'italic' },
|
|
|
|
// Participants
|
|
participantRow: { flexDirection: 'row', alignItems: 'center', marginBottom: 12 },
|
|
avatarPlaceholder: { width: 40, height: 40, borderRadius: 20, backgroundColor: '#6366F1', justifyContent: 'center', alignItems: 'center', marginRight: 12 },
|
|
pName: { fontSize: 16, fontFamily: FontsFamily.nunitoBold, color: theme.colors.textPrimary },
|
|
pRole: { fontSize: 12, color: theme.colors.textSecondary },
|
|
viewAllBtn: { paddingVertical: 8, alignItems: 'center', borderTopWidth: 1, borderTopColor: '#f0f0f0', marginTop: 8 },
|
|
viewAllText: { color: theme.colors.primary, fontFamily: FontsFamily.nunitoBold },
|
|
|
|
// Actions
|
|
actionsContainer: { padding: 20 },
|
|
primaryBtn: { backgroundColor: theme.colors.primary, padding: 16, borderRadius: 12, alignItems: 'center', elevation: 2, marginBottom: 12 },
|
|
btnText: { color: theme.colors.white, fontSize: 16, fontFamily: FontsFamily.nunitoBold },
|
|
secondaryBtn: { backgroundColor: '#f0f0f0', padding: 16, borderRadius: 12, alignItems: 'center' },
|
|
secondaryBtnText: { color: theme.colors.textPrimary, fontSize: 16, fontFamily: FontsFamily.nunitoBold },
|
|
|
|
// Modal
|
|
modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'flex-end' },
|
|
modalContainer: { backgroundColor: theme.colors.background, borderTopLeftRadius: 24, borderTopRightRadius: 24, height: '80%', padding: 20 },
|
|
modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 },
|
|
modalTitle: { fontSize: 20, fontFamily: FontsFamily.nunitoBold, color: theme.colors.textPrimary },
|
|
closeBtn: { padding: 8, backgroundColor: '#f5f5f5', borderRadius: 20 },
|
|
|
|
// New Styles
|
|
hintContainer: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#FFFBEB', padding: 12, borderRadius: 12, marginBottom: 16, borderLeftWidth: 4, borderLeftColor: '#F59E0B' },
|
|
hintText: { color: '#B45309', fontSize: 13, fontFamily: FontsFamily.nunitoMedium, marginLeft: 8, flex: 1 },
|
|
|
|
floatingBtnContainer: {
|
|
position: 'absolute',
|
|
bottom: 90,
|
|
left: 20,
|
|
right: 20,
|
|
alignItems: 'center',
|
|
},
|
|
floatingActivateBtn: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
backgroundColor: theme.colors.primary,
|
|
paddingVertical: 16,
|
|
paddingHorizontal: 24,
|
|
borderRadius: 16,
|
|
width: '100%',
|
|
elevation: 8,
|
|
shadowColor: theme.colors.primary,
|
|
shadowOffset: { width: 0, height: 4 },
|
|
shadowOpacity: 0.3,
|
|
shadowRadius: 8,
|
|
},
|
|
// Invite & Tooltip
|
|
inviteContainer: {
|
|
position: 'relative',
|
|
alignItems: 'center'
|
|
},
|
|
inviteTooltip: {
|
|
position: 'absolute',
|
|
top: 45,
|
|
right: -10,
|
|
backgroundColor: '#d5d5d5ff',
|
|
paddingHorizontal: 12,
|
|
paddingVertical: 8,
|
|
borderRadius: 8,
|
|
width: 140,
|
|
zIndex: 100,
|
|
elevation: 10,
|
|
},
|
|
tooltipText: {
|
|
color: '#494949ff',
|
|
fontSize: 11,
|
|
fontFamily: FontsFamily.nunitoSemiBold,
|
|
textAlign: 'center',
|
|
},
|
|
tooltipArrow: {
|
|
position: 'absolute',
|
|
top: -6,
|
|
right: 20,
|
|
width: 0,
|
|
height: 0,
|
|
borderLeftWidth: 6,
|
|
borderRightWidth: 6,
|
|
borderBottomWidth: 6,
|
|
borderLeftColor: 'transparent',
|
|
borderRightColor: 'transparent',
|
|
borderBottomColor: '#334155',
|
|
},
|
|
floatingBtnText: {
|
|
color: '#fff',
|
|
fontSize: 18,
|
|
fontFamily: FontsFamily.nunitoBold,
|
|
letterSpacing: 0.5,
|
|
},
|
|
modalContent: { flex: 1 },
|
|
modalFieldContainer: { marginBottom: 16 },
|
|
modalLabel: { fontSize: 14, color: theme.colors.textSecondary, marginBottom: 8, fontFamily: FontsFamily.nunitoBold },
|
|
modalInput: { borderWidth: 1, borderColor: '#e0e0e0', borderRadius: 12, padding: 14, fontSize: 16, color: theme.colors.textPrimary, backgroundColor: '#f9f9f9', fontFamily: FontsFamily.nunitoMedium },
|
|
chipContainer: { flexDirection: 'row', flexWrap: 'wrap', gap: 10 },
|
|
chip: { paddingHorizontal: 16, paddingVertical: 10, borderRadius: 20, backgroundColor: '#f0f0f0', borderWidth: 1, borderColor: '#e0e0e0' },
|
|
activeChip: { backgroundColor: theme.colors.primary, borderColor: theme.colors.primary },
|
|
chipText: { fontSize: 14, fontFamily: FontsFamily.nunitoMedium, color: theme.colors.textSecondary },
|
|
activeChipText: { color: theme.colors.white, fontFamily: FontsFamily.nunitoBold },
|
|
modalFooter: { borderTopWidth: 1, borderTopColor: '#eee', paddingTop: 16 },
|
|
saveBtn: { backgroundColor: theme.colors.primary, padding: 16, borderRadius: 12, alignItems: 'center' },
|
|
saveBtnText: { color: theme.colors.white, fontSize: 16, fontFamily: FontsFamily.nunitoBold }
|
|
});
|
|
|
|
export default EditGroupScreen;
|