feat(app): integrate api

This commit is contained in:
uttam05111990 2026-07-31 17:37:28 +05:30
parent 1f1b9aa264
commit 461feba0ba
30 changed files with 1074 additions and 53 deletions

View File

@ -0,0 +1,10 @@
import { DashboardProjectActivityItem, DashboardProjectActivityPayload } from '@interfaces';
import { api } from '@utils';
export const getDashboardProjectActivityApi = async (
payload: DashboardProjectActivityPayload,
): Promise<DashboardProjectActivityItem[]> => {
return await api.get<DashboardProjectActivityItem[]>(
`/api/project_activity_log/0/${payload.staff_id}`,
);
};

View File

@ -6,6 +6,7 @@ export * from './customersApi';
export * from './fcmTokenApi'; export * from './fcmTokenApi';
export * from './notificationApi'; export * from './notificationApi';
export * from './dashboardLeadCountApi'; export * from './dashboardLeadCountApi';
export * from './dashboardProjectActivityApi';

View File

@ -1,4 +1,4 @@
import { LeadItem, LeadCountItem, AddLeadPayload, AddLeadResponse } from '@interfaces'; import { LeadItem, LeadCountItem, AddLeadPayload, AddLeadResponse, UpdateLeadResponse } from '@interfaces';
import { api } from '@utils'; import { api } from '@utils';
export const getLeadsApi = async ( export const getLeadsApi = async (
@ -37,3 +37,32 @@ export const addLeadApi = async (
return await api.post<AddLeadResponse>('/api/leads', formData); return await api.post<AddLeadResponse>('/api/leads', formData);
}; };
export const updateLeadApi = async (
lead: LeadItem,
newStatusId: string,
): Promise<UpdateLeadResponse> => {
const body = {
source: lead.source || '0',
status: newStatusId,
name: lead.name || '',
assigned: lead.assigned || '',
tags: '',
title: lead.title || '',
email: lead.email || '',
website: lead.website || '',
phonenumber: lead.phonenumber || '',
company: lead.company || '',
address: lead.address || '',
city: lead.city || '',
state: lead.state || '',
country: lead.country || '0',
default_language: lead.default_language || '',
description: lead.description || '',
is_public: lead.is_public || '',
zip: lead.zip || '',
};
const url = `/api/leads/${lead.id}`;
return await api.put<UpdateLeadResponse>(url, body);
};

View File

@ -14,6 +14,7 @@ export * from './customerItemCard';
export * from './customerDetailHeader'; export * from './customerDetailHeader';
export * from './notificationItem'; export * from './notificationItem';
export * from './leadDistribution'; export * from './leadDistribution';
export * from './projectActivityFeed';

View File

@ -3,10 +3,12 @@ export interface LeadDetailHeaderProps {
company?: string; company?: string;
email?: string; email?: string;
phonenumber?: string; phonenumber?: string;
statusId?: string;
statusName: string; statusName: string;
statusColor: string; statusColor: string;
avatarColor?: string; avatarColor?: string;
onStatusPress?: () => void; onStatusPress?: () => void;
onEmailPress?: () => void; onEmailPress?: () => void;
onPhonePress?: () => void; onPhonePress?: () => void;
onStatusChange?: (statusId: string) => void;
} }

View File

@ -75,6 +75,64 @@ export const getStyles = (colors: ThemeColors) =>
textTransform: 'uppercase', textTransform: 'uppercase',
letterSpacing: 0.3, letterSpacing: 0.3,
}, },
statusDropdownButton: {
flexDirection: 'row',
alignItems: 'center',
gap: 5,
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 20,
borderWidth: 1,
flexShrink: 0,
},
statusDropdownText: {
fontSize: 10,
fontWeight: '700',
textTransform: 'uppercase',
letterSpacing: 0.3,
},
menuOverlay: {
position: 'absolute',
top: 36,
right: 0,
backgroundColor: colors.card,
borderRadius: 12,
borderWidth: 1,
borderColor: colors.border,
zIndex: 9999,
elevation: 20,
shadowColor: '#000000',
shadowOffset: { width: 0, height: 6 },
shadowOpacity: 0.15,
shadowRadius: 10,
minWidth: 140,
paddingVertical: 4,
},
menuItem: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
paddingVertical: 9,
paddingHorizontal: 12,
},
menuItemActive: {
backgroundColor: `${colors.icon}08`,
},
menuItemText: {
fontSize: 12,
fontWeight: '600',
color: colors.textSecondary,
flex: 1,
},
menuItemTextActive: {
color: colors.text,
fontWeight: '700',
},
menuItemDot: {
width: 8,
height: 8,
borderRadius: 4,
},
actionRow: { actionRow: {
flexDirection: 'row', flexDirection: 'row',
gap: 10, gap: 10,

View File

@ -1,31 +1,51 @@
import React from 'react'; import React, { useState } from 'react';
import { View, Text, TouchableOpacity } from 'react-native'; import { View, Text, TouchableOpacity } from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons'; import Icon from 'react-native-vector-icons/Ionicons';
import { useTheme } from '@theme'; import { useTheme } from '@theme';
import { getStyles } from './leadDetailHeader.styles'; import { getStyles } from './leadDetailHeader.styles';
import { LeadDetailHeaderProps } from './leadDetailHeader.props'; import { LeadDetailHeaderProps } from './leadDetailHeader.props';
import { getInitials, toRgba } from '@utils'; import { getInitials, toRgba } from '@utils';
import { useAppSelector, RootState } from '@store';
export const LeadDetailHeader: React.FC<LeadDetailHeaderProps> = ({ export const LeadDetailHeader: React.FC<LeadDetailHeaderProps> = ({
name, name,
company, company,
email, email,
phonenumber, phonenumber,
statusId,
statusName, statusName,
statusColor, statusColor,
avatarColor, avatarColor,
onStatusPress,
onEmailPress, onEmailPress,
onPhonePress, onPhonePress,
onStatusChange,
}) => { }) => {
const { theme: colors, isDark } = useTheme(); const { theme: colors, isDark } = useTheme();
const styles = getStyles(colors); const styles = getStyles(colors);
const accent = statusColor || avatarColor || '#5B4CF5'; const { items: statusList } = useAppSelector((state: RootState) => state.statusList);
const [selectedStatusId, setSelectedStatusId] = useState(statusId);
const [menuVisible, setMenuVisible] = useState(false);
const currentStatus = statusList.find(s => s.id === selectedStatusId) || {
name: statusName,
color: statusColor,
};
const accent = currentStatus.color || avatarColor || '#5B4CF5';
const badgeBg = toRgba(accent, isDark ? 0.20 : 0.12); const badgeBg = toRgba(accent, isDark ? 0.20 : 0.12);
const handleSelectStatus = (newStatusId: string) => {
setSelectedStatusId(newStatusId);
setMenuVisible(false);
if (onStatusChange) {
onStatusChange(newStatusId);
}
};
return ( return (
<View style={styles.card}> <View style={[styles.card, menuVisible && { zIndex: 100, elevation: 10 }]}>
{/* Top Row: Avatar + Info + Status */} {/* Top Row: Avatar + Info + Status */}
<View style={styles.topRow}> <View style={styles.topRow}>
<View style={[styles.avatar, { backgroundColor: accent }]}> <View style={[styles.avatar, { backgroundColor: accent }]}>
@ -45,17 +65,52 @@ export const LeadDetailHeader: React.FC<LeadDetailHeaderProps> = ({
</Text> </Text>
</View> </View>
<View style={{ position: 'relative' }}>
<TouchableOpacity <TouchableOpacity
onPress={onStatusPress} activeOpacity={0.7}
activeOpacity={onStatusPress ? 0.7 : 1} onPress={() => setMenuVisible(!menuVisible)}
disabled={!onStatusPress}> style={[
<View style={[styles.statusBadge, { backgroundColor: badgeBg }]}> styles.statusDropdownButton,
{
backgroundColor: badgeBg,
borderColor: toRgba(accent, 0.3),
},
]}>
<View style={[styles.statusDot, { backgroundColor: accent }]} /> <View style={[styles.statusDot, { backgroundColor: accent }]} />
<Text style={[styles.statusText, { color: accent }]}> <Text style={[styles.statusDropdownText, { color: accent }]}>
{statusName || 'No Status'} {currentStatus.name || 'No Status'}
</Text> </Text>
</View> <Icon name={menuVisible ? 'chevron-up' : 'chevron-down'} size={10} color={accent} />
</TouchableOpacity> </TouchableOpacity>
{menuVisible && (
<View style={styles.menuOverlay}>
{statusList.map(statusItem => {
const isActive = statusItem.id === selectedStatusId;
const statusItemColor = statusItem.color?.startsWith('#') ? statusItem.color : colors.icon;
return (
<TouchableOpacity
key={statusItem.id}
style={[styles.menuItem, isActive && styles.menuItemActive]}
onPress={() => handleSelectStatus(statusItem.id)}>
<View style={[styles.menuItemDot, { backgroundColor: statusItemColor }]} />
<Text
style={[
styles.menuItemText,
isActive && styles.menuItemTextActive,
]}
numberOfLines={1}>
{statusItem.name}
</Text>
{isActive && (
<Icon name="checkmark" size={12} color={colors.text} />
)}
</TouchableOpacity>
);
})}
</View>
)}
</View>
</View> </View>
{/* Bottom Row: Call + Email Buttons */} {/* Bottom Row: Call + Email Buttons */}

View File

@ -17,11 +17,13 @@ export const LeadDistribution = ({
const [menuVisible, setMenuVisible] = useState(false); const [menuVisible, setMenuVisible] = useState(false);
const actions = [ const actions = [
{ label: 'Day', value: 'day' }, { label: 'Today', value: 'today' },
{ label: 'Week', value: 'week' }, { label: 'Week', value: 'week' },
{ label: 'Month', value: 'month' }, { label: 'Month', value: 'month' },
{ label: 'Quarter', value: 'quarter' }, { label: 'Quarter', value: 'quarter' },
{ label: 'Half year', value: 'halfyear' },
{ label: 'Year', value: 'year' }, { label: 'Year', value: 'year' },
{ label: 'All', value: 'all' },
]; ];
const currentActionLabel = const currentActionLabel =
@ -54,7 +56,7 @@ export const LeadDistribution = ({
}; };
return ( return (
<View style={styles.card}> <View style={[styles.card, menuVisible && { zIndex: 10, elevation: 10 }]}>
{/* Header */} {/* Header */}
<View style={styles.header}> <View style={styles.header}>
<View style={styles.titleRow}> <View style={styles.titleRow}>

View File

@ -3,4 +3,5 @@ import { LeadItem } from '@interfaces';
export interface LeadItemCardProps { export interface LeadItemCardProps {
item: LeadItem; item: LeadItem;
onPress?: () => void; onPress?: () => void;
onStatusChange?: (statusId: string) => void;
} }

View File

@ -77,6 +77,61 @@ export const getStyles = (colors: ThemeColors) =>
fontWeight: '700', fontWeight: '700',
color: colors.icon, color: colors.icon,
}, },
statusDropdownButton: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 20,
borderWidth: 1,
},
statusDropdownText: {
fontSize: 11,
fontWeight: '700',
},
menuOverlay: {
position: 'absolute',
top: 36,
right: 0,
backgroundColor: colors.card,
borderRadius: 12,
borderWidth: 1,
borderColor: colors.border,
zIndex: 9999,
elevation: 20,
shadowColor: '#000000',
shadowOffset: { width: 0, height: 6 },
shadowOpacity: 0.15,
shadowRadius: 10,
minWidth: 140,
paddingVertical: 4,
},
menuItem: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
paddingVertical: 9,
paddingHorizontal: 12,
},
menuItemActive: {
backgroundColor: `${colors.icon}08`,
},
menuItemText: {
fontSize: 12,
fontWeight: '600',
color: colors.textSecondary,
flex: 1,
},
menuItemTextActive: {
color: colors.text,
fontWeight: '700',
},
menuItemDot: {
width: 8,
height: 8,
borderRadius: 4,
},
cardFooter: { cardFooter: {
flexDirection: 'row', flexDirection: 'row',
gap: 10, gap: 10,

View File

@ -1,26 +1,46 @@
import React from 'react'; import React, { useState } from 'react';
import { View, Text, TouchableOpacity } from 'react-native'; import { View, Text, TouchableOpacity } from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons'; import Icon from 'react-native-vector-icons/Ionicons';
import { useTheme } from '@theme'; import { useTheme } from '@theme';
import { getStyles } from './leadItemCard.styles'; import { getStyles } from './leadItemCard.styles';
import { LeadItemCardProps } from './leadItemCard.props'; import { LeadItemCardProps } from './leadItemCard.props';
import { getInitials, handleEmailPress, handlePhonePress, formatDate } from '@utils'; import { getInitials, handleEmailPress, handlePhonePress, formatDate } from '@utils';
import { useAppSelector, RootState } from '@store';
export const LeadItemCard: React.FC<LeadItemCardProps> = ({ item, onPress }) => { export const LeadItemCard: React.FC<LeadItemCardProps> = ({ item, onPress, onStatusChange }) => {
const { theme: colors } = useTheme(); const { theme: colors } = useTheme();
const styles = getStyles(colors); const styles = getStyles(colors);
const { items: statusList } = useAppSelector((state: RootState) => state.statusList);
const [selectedStatusId, setSelectedStatusId] = useState(item.status);
const [menuVisible, setMenuVisible] = useState(false);
const currentStatus = statusList.find(s => s.id === selectedStatusId) || {
name: item.status_name || item.status,
color: item.color || colors.icon,
};
const displayName = item.name || 'No Name'; const displayName = item.name || 'No Name';
const initials = getInitials(displayName); const initials = getInitials(displayName);
const accent = item.color?.startsWith('#') ? item.color : colors.icon; const accent = currentStatus.color?.startsWith('#') ? currentStatus.color : colors.icon;
const handleSelectStatus = (statusId: string) => {
setSelectedStatusId(statusId);
setMenuVisible(false);
if (onStatusChange) {
onStatusChange(statusId);
}
};
return ( return (
<TouchableOpacity <View style={[styles.customerCard, menuVisible && { zIndex: 100, elevation: 10 }]}>
activeOpacity={onPress ? 0.8 : 1}
onPress={onPress}
style={styles.customerCard}>
<View style={styles.cardHeader}> <View style={styles.cardHeader}>
<View style={styles.cardHeaderLeft}> <TouchableOpacity
activeOpacity={onPress ? 0.7 : 1}
onPress={onPress}
disabled={!onPress}
style={styles.cardHeaderLeft}>
<View style={[styles.avatarCircle, { backgroundColor: accent }]}> <View style={[styles.avatarCircle, { backgroundColor: accent }]}>
<Text style={styles.avatarInitial}>{initials}</Text> <Text style={styles.avatarInitial}>{initials}</Text>
</View> </View>
@ -51,16 +71,54 @@ export const LeadItemCard: React.FC<LeadItemCardProps> = ({ item, onPress }) =>
</Text> </Text>
) : null} ) : null}
</View> </View>
</View> </TouchableOpacity>
<View style={styles.cardHeaderRight}> <View style={styles.cardHeaderRight}>
{item.status_name || item.status ? ( <View style={{ position: 'relative' }}>
<View style={[styles.badge, { backgroundColor: `${accent}15` }]}> <TouchableOpacity
<Text style={[styles.badgeText, { color: accent }]}> activeOpacity={0.7}
{item.status_name || item.status} onPress={() => setMenuVisible(!menuVisible)}
style={[
styles.statusDropdownButton,
{
backgroundColor: `${accent}12`,
borderColor: `${accent}40`,
},
]}>
<Text style={[styles.statusDropdownText, { color: accent }]}>
{currentStatus.name}
</Text> </Text>
<Icon name={menuVisible ? 'chevron-up' : 'chevron-down'} size={10} color={accent} />
</TouchableOpacity>
{menuVisible && (
<View style={styles.menuOverlay}>
{statusList.map(statusItem => {
const isActive = statusItem.id === selectedStatusId;
const statusColor = statusItem.color?.startsWith('#') ? statusItem.color : colors.icon;
return (
<TouchableOpacity
key={statusItem.id}
style={[styles.menuItem, isActive && styles.menuItemActive]}
onPress={() => handleSelectStatus(statusItem.id)}>
<View style={[styles.menuItemDot, { backgroundColor: statusColor }]} />
<Text
style={[
styles.menuItemText,
isActive && styles.menuItemTextActive,
]}
numberOfLines={1}>
{statusItem.name}
</Text>
{isActive && (
<Icon name="checkmark" size={12} color={colors.text} />
)}
</TouchableOpacity>
);
})}
</View>
)}
</View> </View>
) : null}
</View> </View>
</View> </View>
@ -79,6 +137,6 @@ export const LeadItemCard: React.FC<LeadItemCardProps> = ({ item, onPress }) =>
<Text style={styles.emailButtonText}>Email</Text> <Text style={styles.emailButtonText}>Email</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</TouchableOpacity> </View>
); );
}; };

View File

@ -0,0 +1,2 @@
export * from './projectActivityFeed';
export * from './projectActivityFeed.props';

View File

@ -0,0 +1,6 @@
import { DashboardProjectActivityItem } from '@interfaces';
export interface ProjectActivityFeedProps {
data: DashboardProjectActivityItem[];
loading?: boolean;
}

View File

@ -0,0 +1,250 @@
import { StyleSheet } from 'react-native';
import { ThemeColors, colors as globalColors } from '@theme';
export const getStyles = (colors: ThemeColors, isDark: boolean) =>
StyleSheet.create({
/* ── Card wrapper ── */
card: {
backgroundColor: colors.card,
borderRadius: 20,
marginBottom: 20,
shadowColor: isDark ? '#000000' : '#0F172A',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: isDark ? 0.25 : 0.08,
shadowRadius: 12,
elevation: 4,
borderWidth: 1,
borderColor: colors.border,
},
/* ── Header ── */
header: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 16,
paddingTop: 16,
paddingBottom: 14,
gap: 10,
},
titleIconBadge: {
width: 34,
height: 34,
borderRadius: 10,
backgroundColor: globalColors.accent,
justifyContent: 'center',
alignItems: 'center',
shadowColor: globalColors.accent,
shadowOffset: { width: 0, height: 3 },
shadowOpacity: 0.4,
shadowRadius: 6,
elevation: 4,
},
titleGroup: {
flex: 1,
},
title: {
fontSize: 15,
fontWeight: '700',
color: colors.text,
letterSpacing: 0.1,
},
subtitle: {
fontSize: 11,
fontWeight: '500',
color: colors.textMuted,
marginTop: 1,
letterSpacing: 0.2,
},
countBadge: {
paddingHorizontal: 9,
paddingVertical: 4,
borderRadius: 20,
backgroundColor: isDark
? `${globalColors.accent}28`
: `${globalColors.accent}15`,
borderWidth: 1,
borderColor: isDark
? `${globalColors.accent}55`
: `${globalColors.accent}35`,
},
countBadgeText: {
fontSize: 12,
fontWeight: '700',
color: globalColors.accent,
},
/* ── Divider ── */
divider: {
height: 1,
backgroundColor: colors.border,
marginHorizontal: 16,
marginBottom: 4,
opacity: isDark ? 0.5 : 0.8,
},
/* ── Timeline list ── */
listContent: {
paddingHorizontal: 16,
paddingBottom: 12,
paddingTop: 4,
},
/* ── Timeline row ── */
row: {
flexDirection: 'row',
paddingVertical: 12,
},
/* ── Left column: avatar + connector line ── */
leftCol: {
width: 38,
alignItems: 'center',
},
avatar: {
width: 34,
height: 34,
borderRadius: 17,
justifyContent: 'center',
alignItems: 'center',
},
avatarText: {
fontSize: 13,
fontWeight: '800',
color: '#FFFFFF',
},
connector: {
width: 2,
flex: 1,
marginTop: 4,
borderRadius: 1,
backgroundColor: colors.border,
opacity: isDark ? 0.5 : 0.7,
},
/* ── Right column: content bubble ── */
rightCol: {
flex: 1,
marginLeft: 10,
},
bubble: {
backgroundColor: isDark
? `${globalColors.primary}12`
: `${globalColors.primary}07`,
borderRadius: 14,
borderWidth: 1,
borderColor: isDark
? `${globalColors.primary}30`
: `${globalColors.primary}18`,
padding: 12,
},
/* row inside bubble: name + time */
bubbleHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 4,
},
staffName: {
fontSize: 13,
fontWeight: '700',
color: colors.text,
flex: 1,
marginRight: 6,
},
timeAgo: {
fontSize: 10,
fontWeight: '600',
color: colors.textMuted,
letterSpacing: 0.2,
},
/* description */
descriptionRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 5,
marginBottom: 6,
},
actionDot: {
width: 5,
height: 5,
borderRadius: 2.5,
backgroundColor: globalColors.accent,
},
description: {
fontSize: 13,
fontWeight: '500',
color: colors.textSecondary,
flex: 1,
},
/* project chip */
projectChip: {
flexDirection: 'row',
alignItems: 'center',
gap: 5,
backgroundColor: isDark
? `${globalColors.accentAlt}18`
: `${globalColors.accentAlt}12`,
borderRadius: 20,
paddingHorizontal: 9,
paddingVertical: 4,
alignSelf: 'flex-start',
borderWidth: 1,
borderColor: isDark
? `${globalColors.accentAlt}40`
: `${globalColors.accentAlt}30`,
marginBottom: 6,
},
projectChipText: {
fontSize: 10,
fontWeight: '700',
color: globalColors.accentAlt,
letterSpacing: 0.2,
},
/* additional data tag */
additionalTag: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
},
additionalText: {
fontSize: 11,
fontWeight: '500',
color: colors.textMuted,
flex: 1,
},
/* ── Empty / Loader states ── */
stateContainer: {
paddingVertical: 32,
alignItems: 'center',
justifyContent: 'center',
},
emptyText: {
fontSize: 13,
color: colors.textMuted,
marginTop: 8,
fontWeight: '500',
},
/* ── Footer ── */
footer: {
borderTopWidth: 1,
borderTopColor: colors.border,
paddingVertical: 12,
alignItems: 'center',
justifyContent: 'center',
},
footerButton: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
},
footerButtonText: {
fontSize: 13,
fontWeight: '700',
color: globalColors.primary,
},
});

View File

@ -0,0 +1,185 @@
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>
);
};

View File

@ -107,7 +107,7 @@ export const CustomerDetailsScreen = () => {
<> <>
<TouchableOpacity <TouchableOpacity
style={styles.infoRow} style={styles.infoRow}
onPress={() => handleEmailPress(customer.email)} // onPress={() => handleEmailPress(customer.email)}
activeOpacity={0.7}> activeOpacity={0.7}>
<View style={styles.infoIconWrap}> <View style={styles.infoIconWrap}>
<Icon name="mail-outline" size={16} color={colors.icon} /> <Icon name="mail-outline" size={16} color={colors.icon} />
@ -125,7 +125,7 @@ export const CustomerDetailsScreen = () => {
<> <>
<TouchableOpacity <TouchableOpacity
style={styles.infoRow} style={styles.infoRow}
onPress={() => handlePhonePress(customer.phonenumber)} // onPress={() => handlePhonePress(customer.phonenumber)}
activeOpacity={0.7}> activeOpacity={0.7}>
<View style={styles.infoIconWrap}> <View style={styles.infoIconWrap}>
<Icon name="call-outline" size={16} color={colors.icon} /> <Icon name="call-outline" size={16} color={colors.icon} />
@ -143,7 +143,7 @@ export const CustomerDetailsScreen = () => {
<> <>
<TouchableOpacity <TouchableOpacity
style={styles.infoRow} style={styles.infoRow}
onPress={() => handleWebsitePress(customer.website)} // onPress={() => handleWebsitePress(customer.website)}
activeOpacity={0.7}> activeOpacity={0.7}>
<View style={styles.infoIconWrap}> <View style={styles.infoIconWrap}>
<Icon name="globe-outline" size={16} color={colors.icon} /> <Icon name="globe-outline" size={16} color={colors.icon} />

View File

@ -2,9 +2,9 @@ import React, { useEffect, useState } from 'react';
import { ScrollView, View, Text, InteractionManager } from 'react-native'; import { ScrollView, View, Text, InteractionManager } from 'react-native';
import { getStyles } from './dashboard.styles'; import { getStyles } from './dashboard.styles';
import { useTheme } from '../../theme'; import { useTheme } from '../../theme';
import { LeadDistribution } from '@components'; import { LeadDistribution, ProjectActivityFeed } from '@components';
import { useAppDispatch, useAppSelector, RootState } from '@store'; import { useAppDispatch, useAppSelector, RootState } from '@store';
import { getDashboardLeadCount } from './thunk'; import { getDashboardLeadCount, getDashboardProjectActivity } from './thunk';
import { NotificationService } from '@services'; import { NotificationService } from '@services';
export const DashboardScreen = () => { export const DashboardScreen = () => {
@ -12,10 +12,10 @@ export const DashboardScreen = () => {
const { theme: colors } = useTheme(); const { theme: colors } = useTheme();
const styles = getStyles(colors); const styles = getStyles(colors);
const [selectedAction, setSelectedAction] = useState<string>('year'); const [selectedAction, setSelectedAction] = useState<string>('all');
const user_data = useAppSelector((state: RootState) => state.auth.user_data); const user_data = useAppSelector((state: RootState) => state.auth.user_data);
const { leadCountData, loading, error } = useAppSelector( const { leadCountData, loading, error, projectActivityData, activityLoading, activityError } = useAppSelector(
(state: RootState) => state.dashboard, (state: RootState) => state.dashboard,
); );
@ -36,6 +36,17 @@ export const DashboardScreen = () => {
); );
} }
}, [dispatch, selectedAction, user_data]); }, [dispatch, selectedAction, user_data]);
useEffect(() => {
if (user_data?.staffid) {
dispatch(
getDashboardProjectActivity({
staff_id: user_data.staffid,
}),
);
}
}, [dispatch, user_data]);
return ( return (
<ScrollView style={styles.container} contentContainerStyle={styles.content}> <ScrollView style={styles.container} contentContainerStyle={styles.content}>
{error && ( {error && (
@ -51,6 +62,18 @@ export const DashboardScreen = () => {
activeAction={selectedAction} activeAction={selectedAction}
onActionChange={setSelectedAction} onActionChange={setSelectedAction}
/> />
{activityError && (
<View style={styles.errorContainer}>
<Text style={styles.errorText}>{activityError}</Text>
</View>
)}
{/* Project Activity Feed */}
<ProjectActivityFeed
data={projectActivityData}
loading={activityLoading}
/>
</ScrollView> </ScrollView>
); );
}; };

View File

@ -1,21 +1,28 @@
import { createReducer } from '@reduxjs/toolkit'; import { createReducer } from '@reduxjs/toolkit';
import { DashboardLeadCountItem } from '@interfaces'; import { DashboardLeadCountItem, DashboardProjectActivityItem } from '@interfaces';
import { getDashboardLeadCount, resetDashboardState } from './thunk'; import { getDashboardLeadCount, getDashboardProjectActivity, resetDashboardState } from './thunk';
export interface DashboardState { export interface DashboardState {
leadCountData: DashboardLeadCountItem[]; leadCountData: DashboardLeadCountItem[];
projectActivityData: DashboardProjectActivityItem[];
loading: boolean; loading: boolean;
activityLoading: boolean;
error: string | null; error: string | null;
activityError: string | null;
} }
const initialState: DashboardState = { const initialState: DashboardState = {
leadCountData: [], leadCountData: [],
projectActivityData: [],
loading: false, loading: false,
activityLoading: false,
error: null, error: null,
activityError: null,
}; };
export const dashboardReducer = createReducer(initialState, builder => { export const dashboardReducer = createReducer(initialState, builder => {
builder builder
// Lead Count
.addCase(getDashboardLeadCount.pending, acc => { .addCase(getDashboardLeadCount.pending, acc => {
acc.loading = true; acc.loading = true;
acc.error = null; acc.error = null;
@ -32,10 +39,31 @@ export const dashboardReducer = createReducer(initialState, builder => {
action.error.message ?? action.error.message ??
'Failed to load dashboard lead count'; 'Failed to load dashboard lead count';
}) })
// Project Activity
.addCase(getDashboardProjectActivity.pending, acc => {
acc.activityLoading = true;
acc.activityError = null;
})
.addCase(getDashboardProjectActivity.fulfilled, (acc, action) => {
acc.activityLoading = false;
acc.projectActivityData = action.payload;
acc.activityError = null;
})
.addCase(getDashboardProjectActivity.rejected, (acc, action) => {
acc.activityLoading = false;
acc.activityError =
(action.payload as string) ??
action.error.message ??
'Failed to load project activity log';
})
// Reset
.addCase(resetDashboardState, acc => { .addCase(resetDashboardState, acc => {
acc.leadCountData = []; acc.leadCountData = [];
acc.projectActivityData = [];
acc.loading = false; acc.loading = false;
acc.activityLoading = false;
acc.error = null; acc.error = null;
acc.activityError = null;
}); });
}); });

View File

@ -1,6 +1,11 @@
import { createAction, createAsyncThunk } from '@reduxjs/toolkit'; import { createAction, createAsyncThunk } from '@reduxjs/toolkit';
import { getDashboardLeadCountApi } from '@api'; import { getDashboardLeadCountApi, getDashboardProjectActivityApi } from '@api';
import { DashboardLeadCountItem, DashboardLeadCountPayload } from '@interfaces'; import {
DashboardLeadCountItem,
DashboardLeadCountPayload,
DashboardProjectActivityItem,
DashboardProjectActivityPayload,
} from '@interfaces';
export const resetDashboardState = createAction('dashboard/resetState'); export const resetDashboardState = createAction('dashboard/resetState');
@ -17,3 +22,17 @@ export const getDashboardLeadCount = createAsyncThunk<
} }
}, },
); );
export const getDashboardProjectActivity = createAsyncThunk<
DashboardProjectActivityItem[],
DashboardProjectActivityPayload
>(
'dashboard/getProjectActivity',
async (payload, { rejectWithValue }) => {
try {
return await getDashboardProjectActivityApi(payload);
} catch (error: any) {
return rejectWithValue(error.message || 'Failed to load project activity log');
}
},
);

View File

@ -1,11 +1,12 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { View, Text, ScrollView, TouchableOpacity } from 'react-native'; import { View, Text, ScrollView, TouchableOpacity, Alert } from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons'; import Icon from 'react-native-vector-icons/Ionicons';
import { RouteProp, useRoute, useNavigation } from '@react-navigation/native'; import { RouteProp, useRoute, useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useTheme } from '@theme'; import { useTheme } from '@theme';
import { useAppDispatch, useAppSelector, RootState } from '@store'; import { useAppDispatch, useAppSelector, RootState } from '@store';
import { getLeadDetails } from './thunk'; import { getLeadDetails } from './thunk';
import { updateLeadStatus } from '../leads/thunk';
import { getStyles } from './leadDetails.styles'; import { getStyles } from './leadDetails.styles';
import { LeadsStackParamList } from '../../navigation/leadsStack'; import { LeadsStackParamList } from '../../navigation/leadsStack';
import { Loader, LeadDetailHeader, ActionCard } from '@components'; import { Loader, LeadDetailHeader, ActionCard } from '@components';
@ -24,6 +25,30 @@ export const LeadDetailsScreen = () => {
const leadId = route.params?.lead?.id; const leadId = route.params?.lead?.id;
const { item: lead, loading, error } = useAppSelector((state: RootState) => state.leadDetails); const { item: lead, loading, error } = useAppSelector((state: RootState) => state.leadDetails);
const user_data = useAppSelector((state: RootState) => state.auth.user_data); const user_data = useAppSelector((state: RootState) => state.auth.user_data);
const { items: statusItems } = useAppSelector((state: RootState) => state.statusList);
const handleStatusChange = (newStatusId: string) => {
if (!lead) return;
const matchedStatus = statusItems.find(s => s.id === newStatusId);
const newStatusName = matchedStatus ? matchedStatus.name : newStatusId;
const newColor = matchedStatus ? matchedStatus.color : colors.icon;
dispatch(
updateLeadStatus({
lead: lead as any,
newStatusId,
newStatusName,
newColor,
}),
)
.unwrap()
.then(res => {
Alert.alert('Success', res.message || 'Status updated successfully');
})
.catch(err => {
Alert.alert('Error', err || 'Failed to update status');
});
};
useEffect(() => { useEffect(() => {
if (leadId && user_data?.staffid) { if (leadId && user_data?.staffid) {
@ -64,10 +89,12 @@ export const LeadDetailsScreen = () => {
company={lead.company} company={lead.company}
email={lead.email} email={lead.email}
phonenumber={lead.phonenumber} phonenumber={lead.phonenumber}
statusId={lead.status}
statusName={lead.status_name || lead.status} statusName={lead.status_name || lead.status}
statusColor={lead.color} statusColor={lead.color}
onEmailPress={() => handleEmailPress(lead.email)} onEmailPress={() => handleEmailPress(lead.email)}
onPhonePress={() => handlePhonePress(lead.phonenumber)} onPhonePress={() => handlePhonePress(lead.phonenumber)}
onStatusChange={handleStatusChange}
/> />
<View style={styles.statsRow}> <View style={styles.statsRow}>
@ -97,7 +124,7 @@ export const LeadDetailsScreen = () => {
<> <>
<TouchableOpacity <TouchableOpacity
style={styles.infoRow} style={styles.infoRow}
onPress={() => handleEmailPress(lead.email)} // onPress={() => handleEmailPress(lead.email)}
activeOpacity={0.7}> activeOpacity={0.7}>
<View style={styles.infoIconWrap}> <View style={styles.infoIconWrap}>
<Icon name="mail-outline" size={15} color={colors.icon} /> <Icon name="mail-outline" size={15} color={colors.icon} />
@ -115,7 +142,7 @@ export const LeadDetailsScreen = () => {
<> <>
<TouchableOpacity <TouchableOpacity
style={styles.infoRow} style={styles.infoRow}
onPress={() => handlePhonePress(lead.phonenumber)} // onPress={() => handlePhonePress(lead.phonenumber)}
activeOpacity={0.7}> activeOpacity={0.7}>
<View style={styles.infoIconWrap}> <View style={styles.infoIconWrap}>
<Icon name="call-outline" size={15} color={colors.icon} /> <Icon name="call-outline" size={15} color={colors.icon} />
@ -133,7 +160,7 @@ export const LeadDetailsScreen = () => {
<> <>
<TouchableOpacity <TouchableOpacity
style={styles.infoRow} style={styles.infoRow}
onPress={() => handleWebsitePress(lead.website)} // onPress={() => handleWebsitePress(lead.website)}
activeOpacity={0.7}> activeOpacity={0.7}>
<View style={styles.infoIconWrap}> <View style={styles.infoIconWrap}>
<Icon name="globe-outline" size={15} color={colors.icon} /> <Icon name="globe-outline" size={15} color={colors.icon} />

View File

@ -1,5 +1,6 @@
import { createReducer } from '@reduxjs/toolkit'; import { createReducer } from '@reduxjs/toolkit';
import { getLeadDetails } from './thunk'; import { getLeadDetails } from './thunk';
import { updateLeadStatus } from '../leads/thunk';
import { LeadDetailsItem } from '@interfaces'; import { LeadDetailsItem } from '@interfaces';
export interface LeadDetailsState { export interface LeadDetailsState {
@ -31,6 +32,13 @@ export const reducers = createReducer(initialState, builder => {
(action.payload as string) ?? (action.payload as string) ??
action.error.message ?? action.error.message ??
'Failed to load lead details'; 'Failed to load lead details';
})
.addCase(updateLeadStatus.fulfilled, (acc, action) => {
if (acc.item && acc.item.id === action.payload.leadId) {
acc.item.status = action.payload.statusId;
acc.item.status_name = action.payload.statusName;
acc.item.color = action.payload.color;
}
}); });
}); });

View File

@ -6,6 +6,7 @@ import {
ScrollView, ScrollView,
TouchableOpacity, TouchableOpacity,
ActivityIndicator, ActivityIndicator,
Alert,
} from 'react-native'; } from 'react-native';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { NativeStackNavigationProp } from '@react-navigation/native-stack';
@ -13,7 +14,7 @@ import Icon from 'react-native-vector-icons/Ionicons';
import { getStyles } from './leads.styles'; import { getStyles } from './leads.styles';
import { useTheme } from '@theme'; import { useTheme } from '@theme';
import { useAppDispatch, useAppSelector, RootState } from '@store'; import { useAppDispatch, useAppSelector, RootState } from '@store';
import { getLeads, getLeadCountList } from './thunk'; import { getLeads, getLeadCountList, updateLeadStatus } from './thunk';
import { LeadItem } from '@interfaces'; import { LeadItem } from '@interfaces';
import { LeadItemCard, SearchInput, Loader, StatCard } from '@components'; import { LeadItemCard, SearchInput, Loader, StatCard } from '@components';
import { LeadsStackParamList } from '../../navigation/leadsStack'; import { LeadsStackParamList } from '../../navigation/leadsStack';
@ -35,6 +36,9 @@ export const LeadsScreen = () => {
const { items, loading, error, counts, countsLoading } = useAppSelector( const { items, loading, error, counts, countsLoading } = useAppSelector(
(state: RootState) => state.leads, (state: RootState) => state.leads,
); );
const { items: statusItems } = useAppSelector(
(state: RootState) => state.statusList,
);
const user_data = useAppSelector((state: RootState) => state.auth.user_data); const user_data = useAppSelector((state: RootState) => state.auth.user_data);
useEffect(() => { useEffect(() => {
@ -67,8 +71,37 @@ export const LeadsScreen = () => {
navigation.navigate(route.leadDetails, { lead }); navigation.navigate(route.leadDetails, { lead });
}; };
const handleStatusChange = (lead: LeadItem, newStatusId: string) => {
const matchedStatus = statusItems.find(s => s.id === newStatusId);
const newStatusName = matchedStatus ? matchedStatus.name : newStatusId;
const newColor = matchedStatus ? matchedStatus.color : colors.icon;
dispatch(
updateLeadStatus({
lead,
newStatusId,
newStatusName,
newColor,
}),
)
.unwrap()
.then(res => {
Alert.alert('Success', res.message || 'Status updated successfully');
if (user_data?.staffid) {
dispatch(getLeadCountList(user_data.staffid));
}
})
.catch(err => {
Alert.alert('Error', err || 'Failed to update status');
});
};
const renderLead = ({ item }: { item: LeadItem }) => ( const renderLead = ({ item }: { item: LeadItem }) => (
<LeadItemCard item={item} onPress={() => handleLeadPress(item)} /> <LeadItemCard
item={item}
onPress={() => handleLeadPress(item)}
onStatusChange={newStatusId => handleStatusChange(item, newStatusId)}
/>
); );
return ( return (

View File

@ -1,5 +1,5 @@
import { createReducer } from '@reduxjs/toolkit'; import { createReducer } from '@reduxjs/toolkit';
import { getLeads, getLeadCountList } from './thunk'; import { getLeads, getLeadCountList, updateLeadStatus } from './thunk';
import { LeadItem, LeadCountItem } from '@interfaces'; import { LeadItem, LeadCountItem } from '@interfaces';
export interface LeadsState { export interface LeadsState {
@ -53,6 +53,19 @@ export const reducers = createReducer(initialState, builder => {
(action.payload as string) ?? (action.payload as string) ??
action.error.message ?? action.error.message ??
'Failed to load lead counts'; 'Failed to load lead counts';
})
.addCase(updateLeadStatus.fulfilled, (acc, action) => {
const { leadId, statusId, statusName, color } = action.payload;
acc.items = acc.items.map(item =>
item.id === leadId
? {
...item,
status: statusId,
status_name: statusName,
color: color,
}
: item,
);
}); });
}); });

View File

@ -1,5 +1,5 @@
import { createAsyncThunk } from '@reduxjs/toolkit'; import { createAsyncThunk } from '@reduxjs/toolkit';
import { getLeadsApi, getLeadCountListApi } from '@api'; import { getLeadsApi, getLeadCountListApi, updateLeadApi } from '@api';
import { LeadItem, LeadCountItem } from '@interfaces'; import { LeadItem, LeadCountItem } from '@interfaces';
export const getLeads = createAsyncThunk< export const getLeads = createAsyncThunk<
@ -23,3 +23,31 @@ export const getLeadCountList = createAsyncThunk<
return rejectWithValue(error.message || 'Failed to load lead counts'); return rejectWithValue(error.message || 'Failed to load lead counts');
} }
}); });
export const updateLeadStatus = createAsyncThunk<
{ leadId: string; statusId: string; statusName: string; color: string; message: string },
{ lead: LeadItem; newStatusId: string; newStatusName: string; newColor: string },
{ rejectValue: string }
>(
'leads/updateStatus',
async (payload, { rejectWithValue }) => {
try {
const { lead, newStatusId } = payload;
const response = await updateLeadApi(lead, newStatusId);
if (response.status) {
return {
leadId: lead.id,
statusId: newStatusId,
statusName: payload.newStatusName,
color: payload.newColor,
message: response.message,
};
} else {
return rejectWithValue(response.message || 'Failed to update status');
}
} catch (error: any) {
return rejectWithValue(error.message || 'Failed to update status');
}
},
);

View File

@ -7,7 +7,7 @@ export interface DashboardLeadCountItem {
count: number; count: number;
} }
export type DashboardLeadCountAction = 'day' | 'week' | 'month' | 'quarter' | 'year'; export type DashboardLeadCountAction = 'today' | 'week' | 'month' | 'quarter' | 'halfyear' | 'year' | 'all';
export interface DashboardLeadCountPayload { export interface DashboardLeadCountPayload {
action: DashboardLeadCountAction | string; action: DashboardLeadCountAction | string;

View File

@ -0,0 +1,20 @@
export interface DashboardProjectActivityItem {
id: string;
project_id: string;
staff_id: string;
contact_id: string;
fullname: string;
dateadded: string;
description: string;
additional_data: string;
project_name: string;
fullname_formatted: string;
actor_url: string;
date_title: string;
date_time_ago: string;
project_url: string;
}
export interface DashboardProjectActivityPayload {
staff_id: string | number;
}

View File

@ -7,5 +7,6 @@ export * from './customers';
export * from './fcmToken'; export * from './fcmToken';
export * from './notification'; export * from './notification';
export * from './dashboardLeadCount'; export * from './dashboardLeadCount';
export * from './dashboardProjectActivity';

View File

@ -77,3 +77,8 @@ export interface AddLeadResponse {
message: string; message: string;
} }
export interface UpdateLeadResponse {
status: boolean;
message: string;
}

View File

@ -0,0 +1,100 @@
import { DashboardProjectActivityItem } from '@interfaces';
export const MOCK_PROJECT_ACTIVITY: DashboardProjectActivityItem[] = [
{
id: '1',
project_id: '28',
staff_id: '5',
contact_id: '0',
fullname: 'John Doe',
dateadded: '2026-07-31 10:15:30',
description: 'Marked task as completed',
additional_data: 'Task: Design database structure',
project_name: 'CRM Mobile App Development',
fullname_formatted: 'John Doe',
actor_url: 'https://your-crm-domain.com/admin/profile/5',
date_title: '2026-07-31 10:15:30',
date_time_ago: '2 hours ago',
project_url: 'https://your-crm-domain.com/admin/projects/view/28',
},
{
id: '2',
project_id: '14',
staff_id: '3',
contact_id: '0',
fullname: 'Sarah Mitchell',
dateadded: '2026-07-31 08:45:00',
description: 'Added a new comment',
additional_data: 'Task: Review API endpoints documentation',
project_name: 'E-Commerce Platform Redesign',
fullname_formatted: 'Sarah Mitchell',
actor_url: 'https://your-crm-domain.com/admin/profile/3',
date_title: '2026-07-31 08:45:00',
date_time_ago: '4 hours ago',
project_url: 'https://your-crm-domain.com/admin/projects/view/14',
},
{
id: '3',
project_id: '7',
staff_id: '9',
contact_id: '0',
fullname: 'Alex Rivera',
dateadded: '2026-07-30 17:30:00',
description: 'Changed task status',
additional_data: 'Task: Integrate payment gateway — moved to In Progress',
project_name: 'FinTech Dashboard v2',
fullname_formatted: 'Alex Rivera',
actor_url: 'https://your-crm-domain.com/admin/profile/9',
date_title: '2026-07-30 17:30:00',
date_time_ago: '1 day ago',
project_url: 'https://your-crm-domain.com/admin/projects/view/7',
},
{
id: '4',
project_id: '21',
staff_id: '2',
contact_id: '0',
fullname: 'Priya Sharma',
dateadded: '2026-07-30 14:00:00',
description: 'Uploaded project file',
additional_data: 'File: UI_Wireframes_v3.fig',
project_name: 'Healthcare Portal',
fullname_formatted: 'Priya Sharma',
actor_url: 'https://your-crm-domain.com/admin/profile/2',
date_title: '2026-07-30 14:00:00',
date_time_ago: '1 day ago',
project_url: 'https://your-crm-domain.com/admin/projects/view/21',
},
{
id: '5',
project_id: '33',
staff_id: '11',
contact_id: '0',
fullname: 'Marcus Webb',
dateadded: '2026-07-29 11:20:00',
description: 'Created a new milestone',
additional_data: 'Milestone: Beta Release — Q3 2026',
project_name: 'SaaS Analytics Suite',
fullname_formatted: 'Marcus Webb',
actor_url: 'https://your-crm-domain.com/admin/profile/11',
date_title: '2026-07-29 11:20:00',
date_time_ago: '2 days ago',
project_url: 'https://your-crm-domain.com/admin/projects/view/33',
},
{
id: '6',
project_id: '28',
staff_id: '7',
contact_id: '0',
fullname: 'Emily Chen',
dateadded: '2026-07-29 09:05:00',
description: 'Left a note on project',
additional_data: 'Note: Awaiting client approval on color palette',
project_name: 'CRM Mobile App Development',
fullname_formatted: 'Emily Chen',
actor_url: 'https://your-crm-domain.com/admin/profile/7',
date_title: '2026-07-29 09:05:00',
date_time_ago: '2 days ago',
project_url: 'https://your-crm-domain.com/admin/projects/view/28',
},
];

View File

@ -8,4 +8,5 @@ export * from './projects';
export * from './proposal'; export * from './proposal';
export * from './tasks'; export * from './tasks';
export * from './tickets'; export * from './tickets';
export * from './notification' export * from './notification';
export * from './dashboardProjectActivity';