186 lines
5.6 KiB
TypeScript
186 lines
5.6 KiB
TypeScript
import React, { useState } from 'react';
|
|
import {
|
|
View,
|
|
Text,
|
|
ActivityIndicator,
|
|
ScrollView,
|
|
TouchableOpacity,
|
|
} from 'react-native';
|
|
import Icon from 'react-native-vector-icons/Ionicons';
|
|
import { useTheme } from '@theme';
|
|
import { DashboardProjectActivityItem } from '@interfaces';
|
|
import { getStyles } from './projectActivityFeed.styles';
|
|
import { ProjectActivityFeedProps } from './projectActivityFeed.props';
|
|
import { colors as globalColors } from '@theme';
|
|
|
|
const AVATAR_COLORS = [
|
|
'#5B4CF5', // indigo
|
|
'#8B5CF6', // violet
|
|
'#06B6D4', // cyan
|
|
'#10B981', // emerald
|
|
'#F59E0B', // amber
|
|
'#EF4444', // red
|
|
'#EC4899', // pink
|
|
];
|
|
|
|
const getInitials = (name: string): string => {
|
|
const parts = name.trim().split(' ');
|
|
if (parts.length >= 2) {
|
|
return `${parts[0][0]}${parts[1][0]}`.toUpperCase();
|
|
}
|
|
return name.slice(0, 2).toUpperCase();
|
|
};
|
|
|
|
const ActivityRow = ({
|
|
item,
|
|
index,
|
|
isLast,
|
|
styles,
|
|
textMutedColor,
|
|
}: {
|
|
item: DashboardProjectActivityItem;
|
|
index: number;
|
|
isLast: boolean;
|
|
styles: ReturnType<typeof getStyles>;
|
|
textMutedColor: string;
|
|
}) => {
|
|
const avatarColor = AVATAR_COLORS[index % AVATAR_COLORS.length];
|
|
const initials = getInitials(item.fullname_formatted || item.fullname);
|
|
|
|
return (
|
|
<View style={styles.row}>
|
|
{/* ── Left: avatar + timeline connector ── */}
|
|
<View style={styles.leftCol}>
|
|
<View style={[styles.avatar, { backgroundColor: avatarColor }]}>
|
|
<Text style={styles.avatarText}>{initials}</Text>
|
|
</View>
|
|
{!isLast && <View style={styles.connector} />}
|
|
</View>
|
|
|
|
{/* ── Right: bubble content ── */}
|
|
<View style={styles.rightCol}>
|
|
<View style={styles.bubble}>
|
|
{/* Name + time */}
|
|
<View style={styles.bubbleHeader}>
|
|
<Text style={styles.staffName} numberOfLines={1}>
|
|
{item.fullname_formatted || item.fullname}
|
|
</Text>
|
|
<Text style={styles.timeAgo}>{item.date_time_ago}</Text>
|
|
</View>
|
|
|
|
{/* Action description */}
|
|
<View style={styles.descriptionRow}>
|
|
<View style={styles.actionDot} />
|
|
<Text style={styles.description} numberOfLines={2}>
|
|
{item.description}
|
|
</Text>
|
|
</View>
|
|
|
|
{/* Project chip */}
|
|
<View style={styles.projectChip}>
|
|
<Icon name="folder-outline" size={10} color={globalColors.accentAlt} />
|
|
<Text style={styles.projectChipText} numberOfLines={1}>
|
|
{item.project_name}
|
|
</Text>
|
|
</View>
|
|
|
|
{/* Additional data tag */}
|
|
{!!item.additional_data && (
|
|
<View style={styles.additionalTag}>
|
|
<Icon name="document-text-outline" size={11} color={textMutedColor} />
|
|
<Text style={styles.additionalText} numberOfLines={1}>
|
|
{item.additional_data}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
</View>
|
|
</View>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
export const ProjectActivityFeed = ({
|
|
data = [],
|
|
loading = false,
|
|
}: ProjectActivityFeedProps) => {
|
|
const { theme: colors, isDark } = useTheme();
|
|
const styles = getStyles(colors, isDark);
|
|
const [expanded, setExpanded] = useState(false);
|
|
|
|
const displayData = expanded ? data : data.slice(0, 5);
|
|
const hasMore = data.length > 5;
|
|
|
|
return (
|
|
<View style={styles.card}>
|
|
{/* ── Header ── */}
|
|
<View style={styles.header}>
|
|
<View style={styles.titleIconBadge}>
|
|
<Icon name="pulse" size={16} color="#FFFFFF" />
|
|
</View>
|
|
<View style={styles.titleGroup}>
|
|
<Text style={styles.title}>Latest Project Activity</Text>
|
|
<Text style={styles.subtitle}>recent actions across projects</Text>
|
|
</View>
|
|
{!loading && data.length > 0 && (
|
|
<View style={styles.countBadge}>
|
|
<Text style={styles.countBadgeText}>{data.length}</Text>
|
|
</View>
|
|
)}
|
|
</View>
|
|
|
|
{/* ── Divider ── */}
|
|
<View style={styles.divider} />
|
|
|
|
{/* ── Body ── */}
|
|
{loading ? (
|
|
<View style={styles.stateContainer}>
|
|
<ActivityIndicator size="large" color={globalColors.accent} />
|
|
</View>
|
|
) : data.length === 0 ? (
|
|
<View style={styles.stateContainer}>
|
|
<Icon name="calendar-outline" size={32} color={colors.textMuted} />
|
|
<Text style={styles.emptyText}>No recent activity</Text>
|
|
</View>
|
|
) : (
|
|
<>
|
|
<ScrollView
|
|
style={styles.listContent}
|
|
scrollEnabled={false}
|
|
showsVerticalScrollIndicator={false}
|
|
>
|
|
{displayData.map((item, index) => (
|
|
<ActivityRow
|
|
key={item.id}
|
|
item={item}
|
|
index={index}
|
|
isLast={index === displayData.length - 1}
|
|
styles={styles}
|
|
textMutedColor={colors.textMuted}
|
|
/>
|
|
))}
|
|
</ScrollView>
|
|
|
|
{hasMore && (
|
|
<View style={styles.footer}>
|
|
<TouchableOpacity
|
|
style={styles.footerButton}
|
|
activeOpacity={0.7}
|
|
onPress={() => setExpanded(!expanded)}
|
|
>
|
|
<Text style={styles.footerButtonText}>
|
|
{expanded ? 'Show Less' : `Show More (${data.length - 5} more)`}
|
|
</Text>
|
|
<Icon
|
|
name={expanded ? 'chevron-up' : 'chevron-down'}
|
|
size={14}
|
|
color={globalColors.primary}
|
|
/>
|
|
</TouchableOpacity>
|
|
</View>
|
|
)}
|
|
</>
|
|
)}
|
|
</View>
|
|
);
|
|
};
|