manage group

This commit is contained in:
Abin Toppo
2025-09-18 13:45:39 +05:30
26 changed files with 2044 additions and 501 deletions
+310
View File
@@ -0,0 +1,310 @@
import { NativeStackScreenProps } from '@react-navigation/native-stack';
import React, { useState, useEffect } from 'react';
import {
View,
Text,
TextInput,
StyleSheet,
ScrollView,
TouchableOpacity,
Alert,
ActivityIndicator,
} from 'react-native';
import { RootStackParamList } from '../../navigation/AppNavigator'; // Adjust path
import { apiCall } from '../../utils/ApiHelper'; // Adjust path
import AppView from '../../components/AppView';
// --- Type Definitions ---
interface Participant {
id: string;
firstName: string;
lastName: string | null;
mobile: string;
role: 'ORGANIZER' | 'USER';
}
export interface GroupDetails {
id: string;
groupName: string;
description: string;
startDate: [number, number, number];
endDate: [number, number, number] | null;
contributionAmount: number;
maxParticipants: number;
latePaymentCharge: number | null;
cycleType: 'QUARTERLY' | 'MONTHLY' | 'WEEKLY' | 'CUSTOM';
visibility: 'PUBLIC' | 'PRIVATE';
organizerFirstName: string;
organizerLastName: string;
customCycleDays: number;
status: string;
totalParticipants: number;
participants: Participant[];
}
export interface EditGroupPayload {
groupName: string;
description: string;
visibility: 'PUBLIC' | 'PRIVATE';
cycleType: 'QUARTERLY' | 'MONTHLY' | 'WEEKLY' | 'CUSTOM';
contributionAmount: number;
maxParticipants: number;
latePaymentCharge: number;
customCycleDays: number;
startDate: string;
endDate: string | null;
}
type EditGroupScreenProps = NativeStackScreenProps<RootStackParamList, 'EditGroup'>;
// --- Helper Components for Read-Only View ---
const InfoRow: React.FC<{ label: string; value: string | number; isStatus?: boolean }> = ({ label, value, isStatus = false }) => (
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>{label}</Text>
<Text
style={[
styles.infoValue,
isStatus && { color: value === 'ACTIVE' ? '#059669' : '#D97706' } // Green for Active, Amber for others
]}
>
{value}
</Text>
</View>
);
const ParticipantRow: React.FC<{ participant: Participant }> = ({ participant }) => (
<View style={styles.participantRow}>
<View>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Text style={styles.participantName}>{`${participant.firstName} ${participant.lastName || ''}`.trim()}</Text>
{participant.role === 'ORGANIZER' && (
<View style={styles.organizerBadge}>
<Text style={styles.organizerBadgeText}>Organizer</Text>
</View>
)}
</View>
<Text style={styles.participantMobile}>{participant.mobile}</Text>
</View>
</View>
);
// --- Main Screen Component ---
const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route, navigation }) => {
const { groupId } = route.params;
// --- State Management ---
const [isEditing, setIsEditing] = useState(false);
const [groupDetails, setGroupDetails] = useState<GroupDetails | null>(null);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
// --- Form State ---
const [groupName, setGroupName] = useState('');
const [description, setDescription] = useState('');
const [contributionAmount, setContributionAmount] = useState('');
const [maxParticipants, setMaxParticipants] = useState('');
const [latePaymentCharge, setLatePaymentCharge] = useState('');
// --- Data Fetching ---
const fetchGroupDetails = async () => {
if (!groupId) {
setError("Group ID not provided.");
setLoading(false);
return;
}
setLoading(true);
try {
const response = await apiCall(
`https://api.godemo.in/FundCollect/api/fund/group/details?groupId=${groupId}`,
'GET',
) as { status: string; data?: GroupDetails; message?: string };
if (response.status === 'success' && response.data) {
setGroupDetails(response.data);
// Populate form state immediately
const data = response.data;
setGroupName(data.groupName);
setDescription(data.description);
setContributionAmount(data.contributionAmount.toString());
setMaxParticipants(data.maxParticipants.toString());
setLatePaymentCharge((data.latePaymentCharge || 0).toString());
} else {
throw new Error(response.message || 'Failed to fetch group data.');
}
} catch (e: any) {
setError(e.message);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchGroupDetails();
}, [groupId]);
// --- Action Handlers ---
const handleCancelEdit = () => {
// Re-populate state from original details to discard any changes
if (groupDetails) {
setGroupName(groupDetails.groupName);
setDescription(groupDetails.description);
setContributionAmount(groupDetails.contributionAmount.toString());
setMaxParticipants(groupDetails.maxParticipants.toString());
setLatePaymentCharge((groupDetails.latePaymentCharge || 0).toString());
}
setIsEditing(false);
};
const handleSaveChanges = async () => {
if (!groupDetails) return;
setSaving(true);
const payload: EditGroupPayload = {
groupName,
description,
visibility: groupDetails.visibility, // Assuming these are not editable
cycleType: groupDetails.cycleType,
contributionAmount: parseFloat(contributionAmount) || 0,
maxParticipants: parseInt(maxParticipants, 10) || 0,
latePaymentCharge: parseFloat(latePaymentCharge) || 0,
customCycleDays: groupDetails.customCycleDays,
startDate: new Date(groupDetails.startDate.join('-')).toISOString().split('T')[0],
endDate: groupDetails.endDate ? new Date(groupDetails.endDate.join('-')).toISOString().split('T')[0] : null,
};
try {
const response = await apiCall(
`https://api.godemo.in/FundCollect/api/fund/groups/${groupDetails.id}/edit`, 'PUT', payload
) as { status: string; message?: string };
if (response.status === 'success') {
Alert.alert('Success', 'Group details have been updated.');
setIsEditing(false);
await fetchGroupDetails(); // Refresh data to show updated view
} else {
throw new Error(response.message || 'Failed to save changes.');
}
} catch (e: any) {
Alert.alert('Save Error', e.message);
} finally {
setSaving(false);
}
};
// --- Render Logic ---
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 (!groupDetails) return null;
return (
<AppView paddingHorizontal={0}>
<ScrollView style={styles.container} contentContainerStyle={styles.contentContainer}>
<View style={styles.header}>
<Text style={styles.headerTitle}>Group Overview</Text>
{!isEditing && groupDetails.status !== 'ACTIVE' ? (
<TouchableOpacity style={styles.editButton} onPress={() => setIsEditing(true)}>
<Text style={styles.editButtonText}>Edit</Text>
</TouchableOpacity>
) : isEditing ? (
<TouchableOpacity onPress={handleCancelEdit}>
<Text style={styles.cancelButtonText}>Cancel</Text>
</TouchableOpacity>
) : null}
</View>
{/* --- Main Details Card --- */}
<View style={styles.card}>
{isEditing ? (
<>
<Text style={styles.inputLabel}>Group Name</Text>
<TextInput style={styles.input} value={groupName} onChangeText={setGroupName} />
<Text style={styles.inputLabel}>Description</Text>
<TextInput style={[styles.input, styles.textArea]} value={description} onChangeText={setDescription} multiline />
</>
) : (
<>
<Text style={styles.groupNameView}>{groupDetails.groupName}</Text>
<Text style={styles.descriptionView}>{groupDetails.description}</Text>
</>
)}
<View style={styles.divider} />
<View style={styles.row}>
<View style={styles.column}>
<Text style={styles.inputLabel}>Contribution</Text>
<Text style={styles.dataText}>{`₹${groupDetails.contributionAmount.toLocaleString('en-IN')}`}</Text>
</View>
<View style={styles.column}>
<Text style={styles.inputLabel}>Cycle</Text>
<Text style={styles.dataText}>{groupDetails.cycleType}</Text>
</View>
</View>
</View>
{/* --- Participants Card --- */}
<View style={styles.card}>
<Text style={styles.cardTitle}>Participants ({groupDetails.totalParticipants}/{groupDetails.maxParticipants})</Text>
{groupDetails.participants.map(p => <ParticipantRow key={`${p.id}-${p.mobile}`} participant={p} />)}
</View>
{/* --- Other Details Card (Always Read-Only) --- */}
<View style={styles.card}>
<InfoRow label="Group Status" value={groupDetails.status} isStatus />
<InfoRow label="Visibility" value={groupDetails.visibility} />
<InfoRow label="Start Date" value={new Date(groupDetails.startDate.join('-')).toLocaleDateString()} />
</View>
{isEditing && (
<TouchableOpacity style={styles.saveButton} onPress={handleSaveChanges} disabled={saving}>
{saving ? <ActivityIndicator color="#FFFFFF" /> : <Text style={styles.saveButtonText}>Save Changes</Text>}
</TouchableOpacity>
)}
</ScrollView>
</AppView>
);
};
// --- Styles ---
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#F3F4F6' },
contentContainer: { paddingVertical: 20, paddingHorizontal: 16 },
centered: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#F3F4F6' },
errorText: { color: '#EF4444', fontSize: 16, textAlign: 'center' },
header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 },
headerTitle: { fontSize: 26, fontWeight: 'bold', color: '#111827' },
editButton: { backgroundColor: '#E0E7FF', paddingHorizontal: 16, paddingVertical: 8, borderRadius: 20 },
editButtonText: { color: '#4338CA', fontSize: 14, fontWeight: '600' },
cancelButtonText: { fontSize: 16, color: '#6B7280', fontWeight: '500' },
card: { backgroundColor: '#FFFFFF', borderRadius: 16, padding: 20, marginBottom: 16, elevation: 1, shadowColor: '#374151', shadowOpacity: 0.05, shadowRadius: 10, shadowOffset: { width: 0, height: 4 } },
cardTitle: { fontSize: 18, fontWeight: '600', color: '#1F2937', marginBottom: 10 },
groupNameView: { fontSize: 22, fontWeight: '700', color: '#1F2937', marginBottom: 8 },
descriptionView: { fontSize: 16, color: '#6B7280', lineHeight: 24 },
divider: { height: 1, backgroundColor: '#E5E7EB', marginVertical: 20 },
row: { flexDirection: 'row', gap: 20 },
column: { flex: 1 },
inputLabel: { fontSize: 14, color: '#6B7280', marginBottom: 8, fontWeight: '500' },
input: { backgroundColor: '#F9FAFB', borderWidth: 1, borderColor: '#D1D5DB', borderRadius: 10, paddingHorizontal: 14, paddingVertical: 12, fontSize: 16, color: '#111827', marginBottom: 16 },
textArea: { height: 80, textAlignVertical: 'top' },
dataText: { fontSize: 18, fontWeight: '600', color: '#111827' },
infoRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: '#F3F4F6' },
infoLabel: { fontSize: 16, color: '#6B7280' },
infoValue: { fontSize: 16, color: '#1F2937', fontWeight: '600' },
participantRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingVertical: 10, borderBottomWidth: 1, borderBottomColor: '#F3F4F6' },
participantName: { fontSize: 16, fontWeight: '600', color: '#111827' },
participantMobile: { fontSize: 14, color: '#6B7280', marginTop: 4 },
organizerBadge: { backgroundColor: '#FEF3C7', paddingHorizontal: 8, paddingVertical: 3, borderRadius: 12, marginLeft: 10 },
organizerBadgeText: { color: '#92400E', fontSize: 12, fontWeight: '500' },
saveButton: { backgroundColor: '#4F46E5', borderRadius: 12, paddingVertical: 16, alignItems: 'center', marginTop: 10, elevation: 2 },
saveButtonText: { color: '#FFFFFF', fontSize: 16, fontWeight: '600' },
});
export default EditGroupScreen;
+5 -1
View File
@@ -49,6 +49,10 @@ const ManagementScreen: FC = () => {
apiCall(DECLINE_REQUEST_URL, "GET") as Promise<ApiResponse<JoinedGroup[]>>, // Assuming this is also needed
apiCall(REQUESTS_URL, "GET") as Promise<ApiResponse<JoinRequest[]>>,
]);
console.log("My Groups:", myGroupsRes.data);
console.log("Joined Groups:", joinedGroupsRes.data);
console.log("Requests:", requestsRes.data);
setMyGroups(myGroupsRes.data || []);
setJoinedGroups(joinedGroupsRes.data || []);
setRequests(requestsRes.data || []);
@@ -91,7 +95,7 @@ const ManagementScreen: FC = () => {
const handleApprove = (req: JoinRequest) => handleAction(APPROVE_REQUEST_URL+req.requestId+"/approve", { requestId: req.requestId }, `${req.userName} has been added.`, "Failed to approve request.","Approve");
const handleDecline = (req: JoinRequest) => handleAction(DECLINE_REQUEST_URL+req.requestId+"/reject?reason=/"+" ", { requestId: req.requestId }, `Request from ${req.userName} has been declined.`, "Failed to decline request.","Decline");
const handleManageGroup = (group: MyGroup) => Alert.alert("Manage Group", `Navigating to manage ${group.groupName}`);
const handleManageGroup = (group: MyGroup) => navigation.navigate('EditGroup', { groupId: group.id });
const renderContent = () => {
if (error) {