Files
circle-collect/src/components/JoinedGroupCard.tsx
T
2026-03-26 15:07:08 +05:30

278 lines
8.7 KiB
TypeScript

import React, { FC } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, ToastAndroid } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { responsiveFontSize } from 'react-native-responsive-dimensions';
import { useTheme } from '../hooks/ui/useTheme';
import { useFetch } from '../hooks/core/useFetch';
import { useCreate } from '../hooks/core/useCreate';
import { CONTRIBUTE } from '../utils/ApiUrls';
import FontsFamily from '../constents/FontsFamily';
interface JoinedGroup {
id: string;
groupName: string;
description: string;
visibility: 'PUBLIC' | 'PRIVATE';
createdByUserId: number;
createdByName: string;
startDate: string | null;
endDate: string | null;
minParticipants: number;
maxParticipants: number;
currentParticipants: number;
contributionAmount: number;
cycleType: 'MONTHLY' | 'WEEKLY' | 'QUARTERLY' | 'CUSTOM';
status: 'ACTIVE' | 'PENDING' | 'COMPLETED'| 'APPROVED';
}
interface JoinedGroupCardProps {
item: JoinedGroup;
hasPaid?: boolean;
}
const formatDate = (dateStr: string | null): string => {
if (!dateStr) return 'N/A';
const [year, month, day] = dateStr.split('-');
const months = [
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
];
const monthIndex = parseInt(month, 10) - 1;
const dayNum = parseInt(day, 10);
return `${dayNum} ${months[monthIndex]} ${year}`;
};
export const JoinedGroupCard: FC<JoinedGroupCardProps> = ({ item }:any) => {
const theme = useTheme();
const navigation = useNavigation<any>();
console.log("item",item)
const { data: payableRes } = useFetch<any>(`${CONTRIBUTE}${item.id}/calculate-contribution`);
const payable = payableRes?.data;
const { mutate: contribute, isPending: loading } = useCreate<any, any>(
`payments/contribute/${item?.id}?amount=${payable?.totalPayable}`,
{
onSuccess: () => {
ToastAndroid.show('Payment successful!', ToastAndroid.SHORT);
},
onError: () => {
ToastAndroid.show('Payment failed. Please try again.', ToastAndroid.SHORT);
},
}
);
const getStatusColors = (status: JoinedGroup['status']) => {
switch (status) {
case 'ACTIVE':
return { bg: theme.palette.greyExtraLight, text: theme.palette.black };
case 'COMPLETED':
return { bg: theme.palette.greyLight, text: theme.palette.greyDark };
default:
return { bg: theme.palette.greyExtraLight, text: theme.palette.greyDark };
}
};
const statusColors = getStatusColors(item.status);
return (
<TouchableOpacity
style={[styles.card, { backgroundColor: theme.colors.card, borderColor: theme.colors.border }]}
onPress={() => navigation.navigate('GroupDetails', { item })}
activeOpacity={0.7}
>
<View style={styles.header}>
<View style={styles.titleContainer}>
<Text style={[styles.groupName, { color: theme.colors.textPrimary }]}>{item.groupName}</Text>
<Text style={[styles.createdBy, { color: theme.colors.textSecondary }]}>
by {item.createdByName}
</Text>
</View>
<View style={[styles.statusBadge, { backgroundColor: statusColors.bg }]}>
<Text style={[styles.statusText, { color: statusColors.text }]}>{item.status}</Text>
</View>
</View>
{/* Contribution Amount — always visible */}
<View style={[styles.infoRow, { borderColor: theme.colors.border }]}>
<View style={styles.infoItem}>
<Text style={[styles.infoLabel, { color: theme.colors.textMuted }]}>Contribution</Text>
<Text style={[styles.infoValue, { color: theme.colors.textPrimary }]}>
UGX {item.contributionAmount.toLocaleString()}
</Text>
</View>
<View style={styles.infoItem}>
<Text style={[styles.infoLabel, { color: theme.colors.textMuted }]}>Duration</Text>
<Text style={[styles.infoValue, { color: theme.colors.textPrimary }]}>{`${item?.cycleType} (${item?.customCycleDays} days)`}</Text>
</View>
</View>
{/* Payment Details — shown when NOT paid and there's an amount due */}
{payable && !payable.hasPaid && payable.totalPayable > 0 && (
<View style={styles.payableSection}>
<View style={styles.payableRow}>
<Text style={[styles.payableLabel, { color: theme.colors.textSecondary }]}>Total Payable</Text>
<Text style={[styles.payableValue, { color: theme.colors.textPrimary }]}>
UGX {payable.totalPayable.toLocaleString()}
</Text>
</View>
{payable.effectiveLateFee > 0 && (
<View style={styles.payableRow}>
<Text style={[styles.payableLabel, { color: theme.colors.warning }]}>Late Fee</Text>
<Text style={[styles.payableValue, { color: theme.colors.warning }]}>
+ UGX {payable.effectiveLateFee.toLocaleString()}
</Text>
</View>
)}
{payable.alreadyPaid > 0 && (
<View style={styles.payableRow}>
<Text style={[styles.payableLabel, { color: theme.colors.success }]}>Already Paid</Text>
<Text style={[styles.payableValue, { color: theme.colors.success }]}>
UGX {payable.alreadyPaid.toLocaleString()}
</Text>
</View>
)}
</View>
)}
<View style={styles.footer}>
<Text style={[styles.dateText, { color: theme.colors.textMuted }]}>
{formatDate(item.startDate)} — {formatDate(item.endDate)}
</Text>
{payable?.hasPaid ? (
<View style={[styles.paidBadge, { backgroundColor: theme.colors.success + '18' }]}>
<Text style={[styles.paidText, { color: theme.colors.success }]}>✓ PAID</Text>
</View>
) : (
item.status !== 'COMPLETED' && (
<TouchableOpacity
style={[styles.payButton, { backgroundColor: theme.colors.primary }]}
onPress={() => {
navigation.navigate('PaymentMode' as any, {
groupId: item.id,
routeFrom: 'GroupDetails'
})
}}
disabled={loading}
>
<Text style={[styles.payButtonText, { color: theme.colors.onPrimary }]}>
{loading ? '...' : 'Pay'}
</Text>
</TouchableOpacity>
)
)}
</View>
</TouchableOpacity>
);
};
const styles = StyleSheet.create({
card: {
borderRadius: 16,
padding: 14,
marginBottom: 12,
marginHorizontal: 12,
borderWidth: 1,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.05,
shadowRadius: 8,
elevation: 2,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: 10,
},
titleContainer: {
flex: 1,
},
groupName: {
fontSize: responsiveFontSize(2),
fontFamily: FontsFamily.nunitoExtraBold,
marginBottom: 2,
},
createdBy: {
fontSize: responsiveFontSize(1.4),
fontFamily: FontsFamily.nunitoRegular,
},
statusBadge: {
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 6,
},
statusText: {
fontSize: responsiveFontSize(1.2),
fontFamily: FontsFamily.nunitoBold,
},
infoRow: {
flexDirection: 'row',
paddingVertical: 10,
borderTopWidth: 1,
borderBottomWidth: 1,
marginBottom: 10,
},
infoItem: {
flex: 1,
},
infoLabel: {
fontSize: responsiveFontSize(1.3),
fontFamily: FontsFamily.nunitoRegular,
marginBottom: 2,
},
infoValue: {
fontSize: responsiveFontSize(1.6),
fontFamily: FontsFamily.nunitoBold,
},
payableSection: {
marginBottom: 12,
gap: 6,
},
payableRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
payableLabel: {
fontSize: responsiveFontSize(1.35),
fontFamily: FontsFamily.nunitoSemiBold,
},
payableValue: {
fontSize: responsiveFontSize(1.5),
fontFamily: FontsFamily.nunitoBold,
},
footer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginTop: 4,
},
dateText: {
fontSize: responsiveFontSize(1.3),
fontFamily: FontsFamily.nunitoMedium,
},
payButton: {
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 8,
minWidth: 80,
alignItems: 'center',
},
payButtonText: {
fontSize: responsiveFontSize(1.5),
fontFamily: FontsFamily.nunitoBold,
},
paidBadge: {
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 6,
},
paidText: {
fontSize: responsiveFontSize(1.3),
fontFamily: FontsFamily.nunitoBold,
letterSpacing: 0.5,
},
});