From 461feba0bad709bd51fce23a320260bf992d766a Mon Sep 17 00:00:00 2001 From: uttam05111990 Date: Fri, 31 Jul 2026 17:37:28 +0530 Subject: [PATCH] feat(app): integrate api --- app/api/dashboardProjectActivityApi.ts | 10 + app/api/index.ts | 1 + app/api/leadsApi.ts | 31 ++- app/components/index.ts | 1 + .../leadDetailHeader.props.ts | 2 + .../leadDetailHeader.styles.ts | 58 ++++ .../leadDetailHeader/leadDetailHeader.tsx | 81 +++++- .../leadDistribution/leadDistribution.tsx | 6 +- .../leadItemCard/leadItemCard.props.ts | 1 + .../leadItemCard/leadItemCard.styles.ts | 55 ++++ app/components/leadItemCard/leadItemCard.tsx | 90 +++++-- app/components/projectActivityFeed/index.ts | 2 + .../projectActivityFeed.props.ts | 6 + .../projectActivityFeed.styles.ts | 250 ++++++++++++++++++ .../projectActivityFeed.tsx | 185 +++++++++++++ .../customerDetails.screen.tsx | 6 +- app/features/dashboard/dashboard.screen.tsx | 31 ++- app/features/dashboard/reducers.ts | 32 ++- app/features/dashboard/thunk.ts | 23 +- .../leadDetails/leadDetails.screen.tsx | 35 ++- app/features/leadDetails/reducers.ts | 8 + app/features/leads/leads.screen.tsx | 37 ++- app/features/leads/reducers.ts | 15 +- app/features/leads/thunk.ts | 30 ++- app/interfaces/dashboardLeadCount.ts | 2 +- app/interfaces/dashboardProjectActivity.ts | 20 ++ app/interfaces/index.ts | 1 + app/interfaces/leads.ts | 5 + app/mock-data/dashboardProjectActivity.ts | 100 +++++++ app/mock-data/index.ts | 3 +- 30 files changed, 1074 insertions(+), 53 deletions(-) create mode 100644 app/api/dashboardProjectActivityApi.ts create mode 100644 app/components/projectActivityFeed/index.ts create mode 100644 app/components/projectActivityFeed/projectActivityFeed.props.ts create mode 100644 app/components/projectActivityFeed/projectActivityFeed.styles.ts create mode 100644 app/components/projectActivityFeed/projectActivityFeed.tsx create mode 100644 app/interfaces/dashboardProjectActivity.ts create mode 100644 app/mock-data/dashboardProjectActivity.ts diff --git a/app/api/dashboardProjectActivityApi.ts b/app/api/dashboardProjectActivityApi.ts new file mode 100644 index 0000000..6965669 --- /dev/null +++ b/app/api/dashboardProjectActivityApi.ts @@ -0,0 +1,10 @@ +import { DashboardProjectActivityItem, DashboardProjectActivityPayload } from '@interfaces'; +import { api } from '@utils'; + +export const getDashboardProjectActivityApi = async ( + payload: DashboardProjectActivityPayload, +): Promise => { + return await api.get( + `/api/project_activity_log/0/${payload.staff_id}`, + ); +}; diff --git a/app/api/index.ts b/app/api/index.ts index d65b86b..b7b124a 100644 --- a/app/api/index.ts +++ b/app/api/index.ts @@ -6,6 +6,7 @@ export * from './customersApi'; export * from './fcmTokenApi'; export * from './notificationApi'; export * from './dashboardLeadCountApi'; +export * from './dashboardProjectActivityApi'; diff --git a/app/api/leadsApi.ts b/app/api/leadsApi.ts index 09db948..7039bd7 100644 --- a/app/api/leadsApi.ts +++ b/app/api/leadsApi.ts @@ -1,4 +1,4 @@ -import { LeadItem, LeadCountItem, AddLeadPayload, AddLeadResponse } from '@interfaces'; +import { LeadItem, LeadCountItem, AddLeadPayload, AddLeadResponse, UpdateLeadResponse } from '@interfaces'; import { api } from '@utils'; export const getLeadsApi = async ( @@ -37,3 +37,32 @@ export const addLeadApi = async ( return await api.post('/api/leads', formData); }; +export const updateLeadApi = async ( + lead: LeadItem, + newStatusId: string, +): Promise => { + 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(url, body); +}; + diff --git a/app/components/index.ts b/app/components/index.ts index aa4d38b..4d44f98 100644 --- a/app/components/index.ts +++ b/app/components/index.ts @@ -14,6 +14,7 @@ export * from './customerItemCard'; export * from './customerDetailHeader'; export * from './notificationItem'; export * from './leadDistribution'; +export * from './projectActivityFeed'; diff --git a/app/components/leadDetailHeader/leadDetailHeader.props.ts b/app/components/leadDetailHeader/leadDetailHeader.props.ts index 1f09d84..af9eb8c 100644 --- a/app/components/leadDetailHeader/leadDetailHeader.props.ts +++ b/app/components/leadDetailHeader/leadDetailHeader.props.ts @@ -3,10 +3,12 @@ export interface LeadDetailHeaderProps { company?: string; email?: string; phonenumber?: string; + statusId?: string; statusName: string; statusColor: string; avatarColor?: string; onStatusPress?: () => void; onEmailPress?: () => void; onPhonePress?: () => void; + onStatusChange?: (statusId: string) => void; } diff --git a/app/components/leadDetailHeader/leadDetailHeader.styles.ts b/app/components/leadDetailHeader/leadDetailHeader.styles.ts index e55ec3f..9190798 100644 --- a/app/components/leadDetailHeader/leadDetailHeader.styles.ts +++ b/app/components/leadDetailHeader/leadDetailHeader.styles.ts @@ -75,6 +75,64 @@ export const getStyles = (colors: ThemeColors) => textTransform: 'uppercase', 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: { flexDirection: 'row', gap: 10, diff --git a/app/components/leadDetailHeader/leadDetailHeader.tsx b/app/components/leadDetailHeader/leadDetailHeader.tsx index 6ce567e..a0f625f 100644 --- a/app/components/leadDetailHeader/leadDetailHeader.tsx +++ b/app/components/leadDetailHeader/leadDetailHeader.tsx @@ -1,31 +1,51 @@ -import React from 'react'; +import React, { useState } from 'react'; import { View, Text, TouchableOpacity } from 'react-native'; import Icon from 'react-native-vector-icons/Ionicons'; import { useTheme } from '@theme'; import { getStyles } from './leadDetailHeader.styles'; import { LeadDetailHeaderProps } from './leadDetailHeader.props'; import { getInitials, toRgba } from '@utils'; +import { useAppSelector, RootState } from '@store'; export const LeadDetailHeader: React.FC = ({ name, company, email, phonenumber, + statusId, statusName, statusColor, avatarColor, - onStatusPress, onEmailPress, onPhonePress, + onStatusChange, }) => { const { theme: colors, isDark } = useTheme(); 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 handleSelectStatus = (newStatusId: string) => { + setSelectedStatusId(newStatusId); + setMenuVisible(false); + if (onStatusChange) { + onStatusChange(newStatusId); + } + }; + return ( - + {/* Top Row: Avatar + Info + Status */} @@ -45,17 +65,52 @@ export const LeadDetailHeader: React.FC = ({ - - + + setMenuVisible(!menuVisible)} + style={[ + styles.statusDropdownButton, + { + backgroundColor: badgeBg, + borderColor: toRgba(accent, 0.3), + }, + ]}> - - {statusName || 'No Status'} + + {currentStatus.name || 'No Status'} - - + + + + {menuVisible && ( + + {statusList.map(statusItem => { + const isActive = statusItem.id === selectedStatusId; + const statusItemColor = statusItem.color?.startsWith('#') ? statusItem.color : colors.icon; + return ( + handleSelectStatus(statusItem.id)}> + + + {statusItem.name} + + {isActive && ( + + )} + + ); + })} + + )} + {/* Bottom Row: Call + Email Buttons */} diff --git a/app/components/leadDistribution/leadDistribution.tsx b/app/components/leadDistribution/leadDistribution.tsx index 40e0e59..387827f 100644 --- a/app/components/leadDistribution/leadDistribution.tsx +++ b/app/components/leadDistribution/leadDistribution.tsx @@ -17,11 +17,13 @@ export const LeadDistribution = ({ const [menuVisible, setMenuVisible] = useState(false); const actions = [ - { label: 'Day', value: 'day' }, + { label: 'Today', value: 'today' }, { label: 'Week', value: 'week' }, { label: 'Month', value: 'month' }, { label: 'Quarter', value: 'quarter' }, + { label: 'Half year', value: 'halfyear' }, { label: 'Year', value: 'year' }, + { label: 'All', value: 'all' }, ]; const currentActionLabel = @@ -54,7 +56,7 @@ export const LeadDistribution = ({ }; return ( - + {/* Header */} diff --git a/app/components/leadItemCard/leadItemCard.props.ts b/app/components/leadItemCard/leadItemCard.props.ts index 230e842..0b2fb53 100644 --- a/app/components/leadItemCard/leadItemCard.props.ts +++ b/app/components/leadItemCard/leadItemCard.props.ts @@ -3,4 +3,5 @@ import { LeadItem } from '@interfaces'; export interface LeadItemCardProps { item: LeadItem; onPress?: () => void; + onStatusChange?: (statusId: string) => void; } diff --git a/app/components/leadItemCard/leadItemCard.styles.ts b/app/components/leadItemCard/leadItemCard.styles.ts index 1e50e42..2166628 100644 --- a/app/components/leadItemCard/leadItemCard.styles.ts +++ b/app/components/leadItemCard/leadItemCard.styles.ts @@ -77,6 +77,61 @@ export const getStyles = (colors: ThemeColors) => fontWeight: '700', 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: { flexDirection: 'row', gap: 10, diff --git a/app/components/leadItemCard/leadItemCard.tsx b/app/components/leadItemCard/leadItemCard.tsx index b52a9c2..06c8976 100644 --- a/app/components/leadItemCard/leadItemCard.tsx +++ b/app/components/leadItemCard/leadItemCard.tsx @@ -1,26 +1,46 @@ -import React from 'react'; +import React, { useState } from 'react'; import { View, Text, TouchableOpacity } from 'react-native'; import Icon from 'react-native-vector-icons/Ionicons'; import { useTheme } from '@theme'; import { getStyles } from './leadItemCard.styles'; import { LeadItemCardProps } from './leadItemCard.props'; import { getInitials, handleEmailPress, handlePhonePress, formatDate } from '@utils'; +import { useAppSelector, RootState } from '@store'; -export const LeadItemCard: React.FC = ({ item, onPress }) => { +export const LeadItemCard: React.FC = ({ item, onPress, onStatusChange }) => { const { theme: colors } = useTheme(); 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 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 ( - + - + {initials} @@ -51,16 +71,54 @@ export const LeadItemCard: React.FC = ({ item, onPress }) => ) : null} - + - {item.status_name || item.status ? ( - - - {item.status_name || item.status} + + setMenuVisible(!menuVisible)} + style={[ + styles.statusDropdownButton, + { + backgroundColor: `${accent}12`, + borderColor: `${accent}40`, + }, + ]}> + + {currentStatus.name} - - ) : null} + + + + {menuVisible && ( + + {statusList.map(statusItem => { + const isActive = statusItem.id === selectedStatusId; + const statusColor = statusItem.color?.startsWith('#') ? statusItem.color : colors.icon; + return ( + handleSelectStatus(statusItem.id)}> + + + {statusItem.name} + + {isActive && ( + + )} + + ); + })} + + )} + @@ -79,6 +137,6 @@ export const LeadItemCard: React.FC = ({ item, onPress }) => Email - + ); }; diff --git a/app/components/projectActivityFeed/index.ts b/app/components/projectActivityFeed/index.ts new file mode 100644 index 0000000..78c2f29 --- /dev/null +++ b/app/components/projectActivityFeed/index.ts @@ -0,0 +1,2 @@ +export * from './projectActivityFeed'; +export * from './projectActivityFeed.props'; diff --git a/app/components/projectActivityFeed/projectActivityFeed.props.ts b/app/components/projectActivityFeed/projectActivityFeed.props.ts new file mode 100644 index 0000000..ffcd43b --- /dev/null +++ b/app/components/projectActivityFeed/projectActivityFeed.props.ts @@ -0,0 +1,6 @@ +import { DashboardProjectActivityItem } from '@interfaces'; + +export interface ProjectActivityFeedProps { + data: DashboardProjectActivityItem[]; + loading?: boolean; +} diff --git a/app/components/projectActivityFeed/projectActivityFeed.styles.ts b/app/components/projectActivityFeed/projectActivityFeed.styles.ts new file mode 100644 index 0000000..e43a7cf --- /dev/null +++ b/app/components/projectActivityFeed/projectActivityFeed.styles.ts @@ -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, + }, + }); diff --git a/app/components/projectActivityFeed/projectActivityFeed.tsx b/app/components/projectActivityFeed/projectActivityFeed.tsx new file mode 100644 index 0000000..69b21d7 --- /dev/null +++ b/app/components/projectActivityFeed/projectActivityFeed.tsx @@ -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; + textMutedColor: string; +}) => { + const avatarColor = AVATAR_COLORS[index % AVATAR_COLORS.length]; + const initials = getInitials(item.fullname_formatted || item.fullname); + + return ( + + {/* ── Left: avatar + timeline connector ── */} + + + {initials} + + {!isLast && } + + + {/* ── Right: bubble content ── */} + + + {/* Name + time */} + + + {item.fullname_formatted || item.fullname} + + {item.date_time_ago} + + + {/* Action description */} + + + + {item.description} + + + + {/* Project chip */} + + + + {item.project_name} + + + + {/* Additional data tag */} + {!!item.additional_data && ( + + + + {item.additional_data} + + + )} + + + + ); +}; + +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 ( + + {/* ── Header ── */} + + + + + + Latest Project Activity + recent actions across projects + + {!loading && data.length > 0 && ( + + {data.length} + + )} + + + {/* ── Divider ── */} + + + {/* ── Body ── */} + {loading ? ( + + + + ) : data.length === 0 ? ( + + + No recent activity + + ) : ( + <> + + {displayData.map((item, index) => ( + + ))} + + + {hasMore && ( + + setExpanded(!expanded)} + > + + {expanded ? 'Show Less' : `Show More (${data.length - 5} more)`} + + + + + )} + + )} + + ); +}; diff --git a/app/features/customerDetails/customerDetails.screen.tsx b/app/features/customerDetails/customerDetails.screen.tsx index 8d4e029..4f955a4 100644 --- a/app/features/customerDetails/customerDetails.screen.tsx +++ b/app/features/customerDetails/customerDetails.screen.tsx @@ -107,7 +107,7 @@ export const CustomerDetailsScreen = () => { <> handleEmailPress(customer.email)} + // onPress={() => handleEmailPress(customer.email)} activeOpacity={0.7}> @@ -125,7 +125,7 @@ export const CustomerDetailsScreen = () => { <> handlePhonePress(customer.phonenumber)} + // onPress={() => handlePhonePress(customer.phonenumber)} activeOpacity={0.7}> @@ -143,7 +143,7 @@ export const CustomerDetailsScreen = () => { <> handleWebsitePress(customer.website)} + // onPress={() => handleWebsitePress(customer.website)} activeOpacity={0.7}> diff --git a/app/features/dashboard/dashboard.screen.tsx b/app/features/dashboard/dashboard.screen.tsx index 05c5773..cfd2b24 100644 --- a/app/features/dashboard/dashboard.screen.tsx +++ b/app/features/dashboard/dashboard.screen.tsx @@ -2,9 +2,9 @@ import React, { useEffect, useState } from 'react'; import { ScrollView, View, Text, InteractionManager } from 'react-native'; import { getStyles } from './dashboard.styles'; import { useTheme } from '../../theme'; -import { LeadDistribution } from '@components'; +import { LeadDistribution, ProjectActivityFeed } from '@components'; import { useAppDispatch, useAppSelector, RootState } from '@store'; -import { getDashboardLeadCount } from './thunk'; +import { getDashboardLeadCount, getDashboardProjectActivity } from './thunk'; import { NotificationService } from '@services'; export const DashboardScreen = () => { @@ -12,10 +12,10 @@ export const DashboardScreen = () => { const { theme: colors } = useTheme(); const styles = getStyles(colors); - const [selectedAction, setSelectedAction] = useState('year'); + const [selectedAction, setSelectedAction] = useState('all'); 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, ); @@ -36,6 +36,17 @@ export const DashboardScreen = () => { ); } }, [dispatch, selectedAction, user_data]); + + useEffect(() => { + if (user_data?.staffid) { + dispatch( + getDashboardProjectActivity({ + staff_id: user_data.staffid, + }), + ); + } + }, [dispatch, user_data]); + return ( {error && ( @@ -51,6 +62,18 @@ export const DashboardScreen = () => { activeAction={selectedAction} onActionChange={setSelectedAction} /> + + {activityError && ( + + {activityError} + + )} + + {/* Project Activity Feed */} + ); }; diff --git a/app/features/dashboard/reducers.ts b/app/features/dashboard/reducers.ts index 02129a4..e7ecfd3 100644 --- a/app/features/dashboard/reducers.ts +++ b/app/features/dashboard/reducers.ts @@ -1,21 +1,28 @@ import { createReducer } from '@reduxjs/toolkit'; -import { DashboardLeadCountItem } from '@interfaces'; -import { getDashboardLeadCount, resetDashboardState } from './thunk'; +import { DashboardLeadCountItem, DashboardProjectActivityItem } from '@interfaces'; +import { getDashboardLeadCount, getDashboardProjectActivity, resetDashboardState } from './thunk'; export interface DashboardState { leadCountData: DashboardLeadCountItem[]; + projectActivityData: DashboardProjectActivityItem[]; loading: boolean; + activityLoading: boolean; error: string | null; + activityError: string | null; } const initialState: DashboardState = { leadCountData: [], + projectActivityData: [], loading: false, + activityLoading: false, error: null, + activityError: null, }; export const dashboardReducer = createReducer(initialState, builder => { builder + // Lead Count .addCase(getDashboardLeadCount.pending, acc => { acc.loading = true; acc.error = null; @@ -32,10 +39,31 @@ export const dashboardReducer = createReducer(initialState, builder => { action.error.message ?? '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 => { acc.leadCountData = []; + acc.projectActivityData = []; acc.loading = false; + acc.activityLoading = false; acc.error = null; + acc.activityError = null; }); }); diff --git a/app/features/dashboard/thunk.ts b/app/features/dashboard/thunk.ts index cef4c29..1c8332f 100644 --- a/app/features/dashboard/thunk.ts +++ b/app/features/dashboard/thunk.ts @@ -1,6 +1,11 @@ import { createAction, createAsyncThunk } from '@reduxjs/toolkit'; -import { getDashboardLeadCountApi } from '@api'; -import { DashboardLeadCountItem, DashboardLeadCountPayload } from '@interfaces'; +import { getDashboardLeadCountApi, getDashboardProjectActivityApi } from '@api'; +import { + DashboardLeadCountItem, + DashboardLeadCountPayload, + DashboardProjectActivityItem, + DashboardProjectActivityPayload, +} from '@interfaces'; 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'); + } + }, +); diff --git a/app/features/leadDetails/leadDetails.screen.tsx b/app/features/leadDetails/leadDetails.screen.tsx index b386c39..37f467e 100644 --- a/app/features/leadDetails/leadDetails.screen.tsx +++ b/app/features/leadDetails/leadDetails.screen.tsx @@ -1,11 +1,12 @@ 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 { RouteProp, useRoute, useNavigation } from '@react-navigation/native'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { useTheme } from '@theme'; import { useAppDispatch, useAppSelector, RootState } from '@store'; import { getLeadDetails } from './thunk'; +import { updateLeadStatus } from '../leads/thunk'; import { getStyles } from './leadDetails.styles'; import { LeadsStackParamList } from '../../navigation/leadsStack'; import { Loader, LeadDetailHeader, ActionCard } from '@components'; @@ -24,6 +25,30 @@ export const LeadDetailsScreen = () => { const leadId = route.params?.lead?.id; const { item: lead, loading, error } = useAppSelector((state: RootState) => state.leadDetails); 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(() => { if (leadId && user_data?.staffid) { @@ -64,10 +89,12 @@ export const LeadDetailsScreen = () => { company={lead.company} email={lead.email} phonenumber={lead.phonenumber} + statusId={lead.status} statusName={lead.status_name || lead.status} statusColor={lead.color} onEmailPress={() => handleEmailPress(lead.email)} onPhonePress={() => handlePhonePress(lead.phonenumber)} + onStatusChange={handleStatusChange} /> @@ -97,7 +124,7 @@ export const LeadDetailsScreen = () => { <> handleEmailPress(lead.email)} + // onPress={() => handleEmailPress(lead.email)} activeOpacity={0.7}> @@ -115,7 +142,7 @@ export const LeadDetailsScreen = () => { <> handlePhonePress(lead.phonenumber)} + // onPress={() => handlePhonePress(lead.phonenumber)} activeOpacity={0.7}> @@ -133,7 +160,7 @@ export const LeadDetailsScreen = () => { <> handleWebsitePress(lead.website)} + // onPress={() => handleWebsitePress(lead.website)} activeOpacity={0.7}> diff --git a/app/features/leadDetails/reducers.ts b/app/features/leadDetails/reducers.ts index 7659bc9..186cc30 100644 --- a/app/features/leadDetails/reducers.ts +++ b/app/features/leadDetails/reducers.ts @@ -1,5 +1,6 @@ import { createReducer } from '@reduxjs/toolkit'; import { getLeadDetails } from './thunk'; +import { updateLeadStatus } from '../leads/thunk'; import { LeadDetailsItem } from '@interfaces'; export interface LeadDetailsState { @@ -31,6 +32,13 @@ export const reducers = createReducer(initialState, builder => { (action.payload as string) ?? action.error.message ?? '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; + } }); }); diff --git a/app/features/leads/leads.screen.tsx b/app/features/leads/leads.screen.tsx index beee1dd..9a8045b 100644 --- a/app/features/leads/leads.screen.tsx +++ b/app/features/leads/leads.screen.tsx @@ -6,6 +6,7 @@ import { ScrollView, TouchableOpacity, ActivityIndicator, + Alert, } from 'react-native'; import { useNavigation } from '@react-navigation/native'; 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 { useTheme } from '@theme'; import { useAppDispatch, useAppSelector, RootState } from '@store'; -import { getLeads, getLeadCountList } from './thunk'; +import { getLeads, getLeadCountList, updateLeadStatus } from './thunk'; import { LeadItem } from '@interfaces'; import { LeadItemCard, SearchInput, Loader, StatCard } from '@components'; import { LeadsStackParamList } from '../../navigation/leadsStack'; @@ -35,6 +36,9 @@ export const LeadsScreen = () => { const { items, loading, error, counts, countsLoading } = useAppSelector( (state: RootState) => state.leads, ); + const { items: statusItems } = useAppSelector( + (state: RootState) => state.statusList, + ); const user_data = useAppSelector((state: RootState) => state.auth.user_data); useEffect(() => { @@ -67,8 +71,37 @@ export const LeadsScreen = () => { 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 }) => ( - handleLeadPress(item)} /> + handleLeadPress(item)} + onStatusChange={newStatusId => handleStatusChange(item, newStatusId)} + /> ); return ( diff --git a/app/features/leads/reducers.ts b/app/features/leads/reducers.ts index 7372e1d..394e639 100644 --- a/app/features/leads/reducers.ts +++ b/app/features/leads/reducers.ts @@ -1,5 +1,5 @@ import { createReducer } from '@reduxjs/toolkit'; -import { getLeads, getLeadCountList } from './thunk'; +import { getLeads, getLeadCountList, updateLeadStatus } from './thunk'; import { LeadItem, LeadCountItem } from '@interfaces'; export interface LeadsState { @@ -53,6 +53,19 @@ export const reducers = createReducer(initialState, builder => { (action.payload as string) ?? action.error.message ?? '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, + ); }); }); diff --git a/app/features/leads/thunk.ts b/app/features/leads/thunk.ts index bb019e0..59bef26 100644 --- a/app/features/leads/thunk.ts +++ b/app/features/leads/thunk.ts @@ -1,5 +1,5 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; -import { getLeadsApi, getLeadCountListApi } from '@api'; +import { getLeadsApi, getLeadCountListApi, updateLeadApi } from '@api'; import { LeadItem, LeadCountItem } from '@interfaces'; export const getLeads = createAsyncThunk< @@ -23,3 +23,31 @@ export const getLeadCountList = createAsyncThunk< 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'); + } + }, +); diff --git a/app/interfaces/dashboardLeadCount.ts b/app/interfaces/dashboardLeadCount.ts index 15f0179..b3ade8f 100644 --- a/app/interfaces/dashboardLeadCount.ts +++ b/app/interfaces/dashboardLeadCount.ts @@ -7,7 +7,7 @@ export interface DashboardLeadCountItem { count: number; } -export type DashboardLeadCountAction = 'day' | 'week' | 'month' | 'quarter' | 'year'; +export type DashboardLeadCountAction = 'today' | 'week' | 'month' | 'quarter' | 'halfyear' | 'year' | 'all'; export interface DashboardLeadCountPayload { action: DashboardLeadCountAction | string; diff --git a/app/interfaces/dashboardProjectActivity.ts b/app/interfaces/dashboardProjectActivity.ts new file mode 100644 index 0000000..8df41a4 --- /dev/null +++ b/app/interfaces/dashboardProjectActivity.ts @@ -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; +} diff --git a/app/interfaces/index.ts b/app/interfaces/index.ts index 96965dc..29e4333 100644 --- a/app/interfaces/index.ts +++ b/app/interfaces/index.ts @@ -7,5 +7,6 @@ export * from './customers'; export * from './fcmToken'; export * from './notification'; export * from './dashboardLeadCount'; +export * from './dashboardProjectActivity'; diff --git a/app/interfaces/leads.ts b/app/interfaces/leads.ts index b0a5dc0..f76bafd 100644 --- a/app/interfaces/leads.ts +++ b/app/interfaces/leads.ts @@ -77,3 +77,8 @@ export interface AddLeadResponse { message: string; } +export interface UpdateLeadResponse { + status: boolean; + message: string; +} + diff --git a/app/mock-data/dashboardProjectActivity.ts b/app/mock-data/dashboardProjectActivity.ts new file mode 100644 index 0000000..700356b --- /dev/null +++ b/app/mock-data/dashboardProjectActivity.ts @@ -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', + }, +]; diff --git a/app/mock-data/index.ts b/app/mock-data/index.ts index a3190fc..ff4fba6 100644 --- a/app/mock-data/index.ts +++ b/app/mock-data/index.ts @@ -8,4 +8,5 @@ export * from './projects'; export * from './proposal'; export * from './tasks'; export * from './tickets'; -export * from './notification' +export * from './notification'; +export * from './dashboardProjectActivity';