feat(app): integrate api
This commit is contained in:
parent
1f1b9aa264
commit
461feba0ba
10
app/api/dashboardProjectActivityApi.ts
Normal file
10
app/api/dashboardProjectActivityApi.ts
Normal 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}`,
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
<TouchableOpacity
|
<View style={{ position: 'relative' }}>
|
||||||
onPress={onStatusPress}
|
<TouchableOpacity
|
||||||
activeOpacity={onStatusPress ? 0.7 : 1}
|
activeOpacity={0.7}
|
||||||
disabled={!onStatusPress}>
|
onPress={() => setMenuVisible(!menuVisible)}
|
||||||
<View style={[styles.statusBadge, { backgroundColor: badgeBg }]}>
|
style={[
|
||||||
|
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 */}
|
||||||
|
|||||||
@ -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}>
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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>
|
||||||
</View>
|
<Icon name={menuVisible ? 'chevron-up' : 'chevron-down'} size={10} color={accent} />
|
||||||
) : null}
|
</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>
|
</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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
2
app/components/projectActivityFeed/index.ts
Normal file
2
app/components/projectActivityFeed/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './projectActivityFeed';
|
||||||
|
export * from './projectActivityFeed.props';
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
import { DashboardProjectActivityItem } from '@interfaces';
|
||||||
|
|
||||||
|
export interface ProjectActivityFeedProps {
|
||||||
|
data: DashboardProjectActivityItem[];
|
||||||
|
loading?: boolean;
|
||||||
|
}
|
||||||
250
app/components/projectActivityFeed/projectActivityFeed.styles.ts
Normal file
250
app/components/projectActivityFeed/projectActivityFeed.styles.ts
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
185
app/components/projectActivityFeed/projectActivityFeed.tsx
Normal file
185
app/components/projectActivityFeed/projectActivityFeed.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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} />
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
@ -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} />
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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 (
|
||||||
|
|||||||
@ -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,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
20
app/interfaces/dashboardProjectActivity.ts
Normal file
20
app/interfaces/dashboardProjectActivity.ts
Normal 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;
|
||||||
|
}
|
||||||
@ -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';
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -77,3 +77,8 @@ export interface AddLeadResponse {
|
|||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UpdateLeadResponse {
|
||||||
|
status: boolean;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
100
app/mock-data/dashboardProjectActivity.ts
Normal file
100
app/mock-data/dashboardProjectActivity.ts
Normal 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',
|
||||||
|
},
|
||||||
|
];
|
||||||
@ -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';
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user