352 lines
16 KiB
TypeScript
352 lines
16 KiB
TypeScript
import { NativeStackScreenProps } from '@react-navigation/native-stack';
|
|
import React, { useState, useEffect, useCallback } 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';
|
|
import { Picker } from '@react-native-picker/picker';
|
|
|
|
// --- 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] | null;
|
|
endDate: [number, number, number] | null;
|
|
contributionAmount: number;
|
|
minParticipants: number;
|
|
maxParticipants: number;
|
|
portalCharge: number | null;
|
|
latePaymentCharge: number | null;
|
|
cycleType: 'QUARTERLY' | 'MONTHLY' | 'WEEKLY' | 'CUSTOM';
|
|
customCycleDays: number;
|
|
withdrawalMethod: 'RANDOM' | 'SEQUENTIAL';
|
|
status: 'PENDING' | 'ACTIVE' | 'COMPLETED' | 'APPROVED';
|
|
visibility: 'PUBLIC' | 'PRIVATE';
|
|
organizerId: string;
|
|
totalParticipants: number;
|
|
participants: Participant[];
|
|
}
|
|
|
|
export interface EditGroupPayload {
|
|
groupName: string;
|
|
description: string;
|
|
maxParticipants: number;
|
|
contributionAmount: number;
|
|
customCycleDays: number | null;
|
|
cycleType: 'QUARTERLY' | 'MONTHLY' | 'WEEKLY' | 'CUSTOM';
|
|
latePaymentCharge: number;
|
|
visibility: 'PUBLIC' | 'PRIVATE';
|
|
}
|
|
|
|
type EditGroupScreenProps = NativeStackScreenProps<RootStackParamList, 'EditGroup'>;
|
|
type EditingField = keyof EditGroupPayload | null;
|
|
|
|
// --- Main Screen Component ---
|
|
const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
|
|
const { groupId } = route.params;
|
|
|
|
const [editingField, setEditingField] = useState<EditingField>(null);
|
|
const [groupDetails, setGroupDetails] = useState<GroupDetails | null>(null);
|
|
const [editableData, setEditableData] = useState<EditGroupPayload>({
|
|
groupName: '',
|
|
description: '',
|
|
maxParticipants: 0,
|
|
contributionAmount: 0,
|
|
customCycleDays: null,
|
|
cycleType: 'MONTHLY',
|
|
latePaymentCharge: 0,
|
|
visibility: 'PRIVATE',
|
|
});
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
const [saving, setSaving] = useState(false);
|
|
const [starting, setStarting] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const fetchGroupDetails = useCallback(async () => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const response = await apiCall(`https://exotic-humpback-mistakenly.ngrok-free.app/api/fund/group/details?groupId=${groupId}`, 'GET') as { status: string; data?: GroupDetails; message?: string };
|
|
if (response.status === 'success' && response.data) {
|
|
setGroupDetails(response.data);
|
|
setEditableData({
|
|
groupName: response.data.groupName,
|
|
description: response.data.description,
|
|
maxParticipants: response.data.maxParticipants,
|
|
contributionAmount: response.data.contributionAmount,
|
|
customCycleDays: response.data.customCycleDays,
|
|
cycleType: response.data.cycleType,
|
|
latePaymentCharge: response.data.latePaymentCharge ?? 0,
|
|
visibility: response.data.visibility,
|
|
});
|
|
} else {
|
|
throw new Error(response.message || 'Failed to fetch group data.');
|
|
}
|
|
} catch (e: any) {
|
|
setError(e.message);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [groupId]);
|
|
|
|
useEffect(() => {
|
|
fetchGroupDetails();
|
|
}, [fetchGroupDetails]);
|
|
|
|
const handleCancel = () => {
|
|
if (groupDetails) {
|
|
setEditableData({
|
|
groupName: groupDetails.groupName,
|
|
description: groupDetails.description,
|
|
maxParticipants: groupDetails.maxParticipants,
|
|
contributionAmount: groupDetails.contributionAmount,
|
|
customCycleDays: groupDetails.customCycleDays,
|
|
cycleType: groupDetails.cycleType,
|
|
latePaymentCharge: groupDetails.latePaymentCharge ?? 0,
|
|
visibility: groupDetails.visibility,
|
|
});
|
|
}
|
|
setEditingField(null);
|
|
};
|
|
|
|
const handleSave = async () => {
|
|
if (!groupDetails || !editingField) return;
|
|
|
|
if (editingField === 'maxParticipants') {
|
|
const newMaxParticipants = Number(editableData.maxParticipants);
|
|
if (newMaxParticipants < groupDetails.totalParticipants) {
|
|
Alert.alert('Invalid Input', `Max participants cannot be less than the current count (${groupDetails.totalParticipants}).`);
|
|
return;
|
|
}
|
|
}
|
|
|
|
setSaving(true);
|
|
try {
|
|
await apiCall(`https://exotic-humpback-mistakenly.ngrok-free.app/apifund/groups/${groupDetails.id}/edit`, 'PUT', editableData);
|
|
Alert.alert('Success', 'Group has been updated.');
|
|
setEditingField(null);
|
|
await fetchGroupDetails();
|
|
} catch (e: any) {
|
|
Alert.alert('Save Error', e.message || 'An unexpected error occurred.');
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
const handleStartGroup = async () => {
|
|
if (!groupDetails) return;
|
|
|
|
Alert.alert(
|
|
"Confirm Start",
|
|
"Are you sure you want to start this group? This action cannot be undone.",
|
|
[
|
|
{ text: "Cancel", style: "cancel" },
|
|
{
|
|
text: "Start",
|
|
onPress: async () => {
|
|
setStarting(true);
|
|
try {
|
|
await apiCall(`https://exotic-humpback-mistakenly.ngrok-free.app/api/fund/group/start?groupId=${groupDetails.id}`, 'GET');
|
|
Alert.alert('Success', 'Group has been started successfully.');
|
|
await fetchGroupDetails();
|
|
} catch (e: any) {
|
|
Alert.alert('Start Error', e.message || 'An unexpected error occurred.');
|
|
} finally {
|
|
setStarting(false);
|
|
}
|
|
},
|
|
style: 'destructive'
|
|
}
|
|
]
|
|
);
|
|
};
|
|
|
|
const handleUpdateField = (field: keyof EditGroupPayload, value: any) => {
|
|
setEditableData(prev => ({ ...prev, [field]: value }));
|
|
};
|
|
|
|
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;
|
|
|
|
const isEditable = groupDetails.status === 'PENDING' || groupDetails.status === 'APPROVED';
|
|
const canStartGroup = groupDetails.status === 'APPROVED' && groupDetails.totalParticipants >= groupDetails.minParticipants;
|
|
|
|
const renderInputField = (field: EditingField) => {
|
|
if (!field || !editableData) return null;
|
|
|
|
const commonProps = { style: styles.input, placeholderTextColor: "#9CA3AF" };
|
|
|
|
switch(field) {
|
|
case 'groupName':
|
|
return <TextInput {...commonProps} value={editableData.groupName} onChangeText={(text) => handleUpdateField('groupName', text)} />;
|
|
case 'description':
|
|
return <TextInput {...commonProps} style={[styles.input, styles.textArea]} value={editableData.description} onChangeText={(text) => handleUpdateField('description', text)} multiline />;
|
|
case 'contributionAmount':
|
|
case 'maxParticipants':
|
|
case 'latePaymentCharge':
|
|
case 'customCycleDays':
|
|
return <TextInput {...commonProps} value={String(editableData[field] ?? '')} onChangeText={(text) => handleUpdateField(field, text ? Number(text.replace(/[^0-9]/g, '')) : 0)} keyboardType="numeric" />;
|
|
case 'cycleType':
|
|
return <Picker selectedValue={editableData.cycleType} onValueChange={(itemValue) => handleUpdateField('cycleType', itemValue)} style={styles.picker}>
|
|
{['MONTHLY', 'WEEKLY', 'QUARTERLY', 'CUSTOM'].map(c => <Picker.Item key={c} label={c} value={c} />)}
|
|
</Picker>;
|
|
case 'visibility':
|
|
return <Picker selectedValue={editableData.visibility} onValueChange={(itemValue) => handleUpdateField('visibility', itemValue)} style={styles.picker}>
|
|
<Picker.Item label="PUBLIC" value="PUBLIC" />
|
|
<Picker.Item label="PRIVATE" value="PRIVATE" />
|
|
</Picker>;
|
|
default: return null;
|
|
}
|
|
}
|
|
|
|
const renderRow = (field: EditingField, label: string, displayValue: string | number) => {
|
|
return (
|
|
<View style={styles.fieldContainer}>
|
|
<View style={styles.infoRow}>
|
|
<Text style={styles.infoLabel}>{label}</Text>
|
|
{editingField !== field && (
|
|
<View style={styles.valueContainer}>
|
|
<Text style={styles.infoValue}>{displayValue}</Text>
|
|
{isEditable && <TouchableOpacity onPress={() => setEditingField(field)} style={styles.editButton}>
|
|
<Text style={styles.editButtonText}>✎</Text>
|
|
</TouchableOpacity>}
|
|
</View>
|
|
)}
|
|
</View>
|
|
{editingField === field && isEditable && (
|
|
<View style={styles.editingContainer}>
|
|
{renderInputField(field)}
|
|
<View style={styles.cardActions}>
|
|
<TouchableOpacity style={[styles.button, styles.cancelButton]} onPress={handleCancel} disabled={saving}>
|
|
<Text style={[styles.buttonText, { color: '#374151' }]}>Cancel</Text>
|
|
</TouchableOpacity>
|
|
<TouchableOpacity style={[styles.button, styles.saveButton]} onPress={handleSave} disabled={saving}>
|
|
{saving ? <ActivityIndicator color="#FFFFFF" size="small" /> : <Text style={styles.buttonText}>Save</Text>}
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
)}
|
|
</View>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<AppView paddingHorizontal={0}>
|
|
<ScrollView style={styles.container} contentContainerStyle={styles.contentContainer}>
|
|
<Text style={styles.headerTitle}>Edit Group</Text>
|
|
|
|
{canStartGroup && (
|
|
<View style={styles.card}>
|
|
<Text style={styles.cardTitle}>Ready to Start?</Text>
|
|
<Text style={styles.startGroupText}>This group has met the minimum participant requirement.</Text>
|
|
<TouchableOpacity style={[styles.button, styles.startButton]} onPress={handleStartGroup} disabled={starting}>
|
|
{starting ? <ActivityIndicator color="#FFFFFF" /> : <Text style={styles.buttonText}>Start Group</Text>}
|
|
</TouchableOpacity>
|
|
</View>
|
|
)}
|
|
|
|
<View style={styles.card}>
|
|
<Text style={styles.cardTitle}>Group Info</Text>
|
|
{renderRow('groupName', 'Group Name', groupDetails.groupName)}
|
|
{renderRow('description', 'Description', groupDetails.description)}
|
|
</View>
|
|
|
|
<View style={styles.card}>
|
|
<Text style={styles.cardTitle}>Financial Rules</Text>
|
|
{renderRow('contributionAmount', 'Contribution', `₹${groupDetails.contributionAmount.toLocaleString('en-IN')}`)}
|
|
{renderRow('maxParticipants', 'Max Participants', `${groupDetails.totalParticipants} / ${groupDetails.maxParticipants}`)}
|
|
{renderRow('latePaymentCharge', 'Late Fee', `₹${(groupDetails.latePaymentCharge ?? 0).toLocaleString('en-IN')}`)}
|
|
</View>
|
|
|
|
<View style={styles.card}>
|
|
<Text style={styles.cardTitle}>Configuration</Text>
|
|
{renderRow('cycleType', 'Payment Cycle', groupDetails.cycleType)}
|
|
{(editableData.cycleType === 'CUSTOM' || groupDetails.cycleType === 'CUSTOM') && renderRow('customCycleDays', 'Custom Days', `${editableData.customCycleDays ?? groupDetails.customCycleDays} days`)}
|
|
{renderRow('visibility', 'Visibility', groupDetails.visibility)}
|
|
</View>
|
|
|
|
<View style={styles.card}>
|
|
<Text style={styles.cardTitle}>Participants ({groupDetails.totalParticipants})</Text>
|
|
{groupDetails.participants.length > 0 ? (
|
|
groupDetails.participants.map(p => (
|
|
<View key={p.id} style={styles.participantRow}>
|
|
<View>
|
|
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
|
<Text style={styles.participantName}>{`${p.firstName} ${p.lastName || ''}`.trim()}</Text>
|
|
{p.role === 'ORGANIZER' && <View style={styles.organizerBadge}><Text style={styles.organizerBadgeText}>Organizer</Text></View>}
|
|
</View>
|
|
<Text style={styles.participantMobile}>{p.mobile}</Text>
|
|
</View>
|
|
</View>
|
|
))
|
|
) : (
|
|
<Text style={styles.noParticipantsText}>No participants yet.</Text>
|
|
)}
|
|
</View>
|
|
</ScrollView>
|
|
</AppView>
|
|
);
|
|
};
|
|
|
|
const styles = StyleSheet.create({
|
|
container: { flex: 1, backgroundColor: '#F3F4F6' },
|
|
contentContainer: { padding: 16, paddingBottom: 40 },
|
|
centered: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
|
errorText: { color: '#EF4444', fontSize: 16, textAlign: 'center' },
|
|
headerTitle: { fontSize: 28, fontWeight: 'bold', color: '#111827', marginBottom: 20 },
|
|
|
|
card: { backgroundColor: '#FFFFFF', borderRadius: 16, padding: 20, marginBottom: 16, shadowColor: '#374151', shadowOpacity: 0.05, shadowRadius: 10 },
|
|
cardTitle: { fontSize: 18, fontWeight: 'bold', color: '#1F2937', marginBottom: 12, borderBottomWidth: 1, borderBottomColor: '#E5E7EB', paddingBottom: 10 },
|
|
|
|
editButton: { padding: 8, marginLeft: 8 },
|
|
editButtonText: { color: '#4F46E5', fontSize: 20, fontWeight: 'bold' },
|
|
|
|
fieldContainer: { borderBottomWidth: 1, borderBottomColor: '#F3F4F6', paddingVertical: 12 },
|
|
infoRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
|
|
infoLabel: { fontSize: 16, color: '#6B7280' },
|
|
infoValue: { fontSize: 16, fontWeight: '600', color: '#111827', textTransform: 'capitalize' },
|
|
valueContainer: { flexDirection: 'row', alignItems: 'center' },
|
|
|
|
editingContainer: { marginTop: 12 },
|
|
input: { backgroundColor: '#F9FAFB', borderWidth: 1, borderColor: '#D1D5DB', borderRadius: 10, padding: 14, fontSize: 16, color: '#111827' },
|
|
textArea: { height: 100, textAlignVertical: 'top' },
|
|
picker: { backgroundColor: '#F9FAFB', borderWidth: 1, borderColor: '#D1D5DB', borderRadius: 10, padding: 0, margin:0 },
|
|
|
|
participantRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 12, 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' },
|
|
noParticipantsText: { fontSize: 15, color: '#6B7280', textAlign: 'center', paddingVertical: 20 },
|
|
|
|
cardActions: { flexDirection: 'row', justifyContent: 'flex-end', marginTop: 20, paddingTop: 16, borderTopWidth: 1, borderTopColor: '#E5E7EB' },
|
|
button: { borderRadius: 10, paddingVertical: 12, paddingHorizontal: 20, marginLeft: 10, minWidth: 80, alignItems: 'center', justifyContent: 'center' },
|
|
saveButton: { backgroundColor: '#4F46E5' },
|
|
cancelButton: { backgroundColor: '#E5E7EB' },
|
|
startButton: { backgroundColor: '#22c55e', marginTop: 10 },
|
|
startGroupText: { fontSize: 15, color: '#6B7280', lineHeight: 22, marginBottom: 10},
|
|
buttonText: { color: '#FFFFFF', fontSize: 15, fontWeight: '600' },
|
|
});
|
|
|
|
export default EditGroupScreen;
|