diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index b240be2..d6be50e 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -14,7 +14,7 @@ import Reports from '@/components/Reports'; import OpportunityBoard from '@/components/OpportunityBoard'; import TargetManager from '@/components/TargetManager'; import IncentiveManager from '@/components/IncentiveManager'; -import FollowupsManager from '@/components/FollowupsManager'; +import ActivitiesManager from '@/components/ActivitiesManager'; import FunnelAnalysisPage from '@/components/FunnelAnalysisPage'; import CallLogs from '@/components/CallLogs'; import Settings from '@/components/Settings'; @@ -53,7 +53,7 @@ export default function DashboardPage() { { id: 'incentives', label: 'Incentives', icon: TrendingUp }, { id: 'reports', label: 'Reports', icon: BarChart }, ...(user?.role === 'ADMIN' || user?.role === 'GENERAL_MANAGER' ? [{ id: 'targets', label: 'Targets', icon: Target }] : []), - { id: 'followups', label: 'Follow-ups', icon: CalendarCheck }, + { id: 'activities', label: 'Activities', icon: CalendarCheck }, { id: 'call-logs', label: 'Call Logs', icon: PhoneCall }, ...(user?.role === 'ADMIN' || user?.role === 'GENERAL_MANAGER' ? [{ id: 'funnel-analysis', label: 'Funnel Analysis', icon: GitMerge }] : []), { id: 'products', label: 'Products', icon: Package }, @@ -102,8 +102,8 @@ export default function DashboardPage() { return ; case 'reports': return ; - case 'followups': - return
; + case 'activities': + return
; case 'call-logs': return ; case 'funnel-analysis': diff --git a/src/components/ActivitiesManager.tsx b/src/components/ActivitiesManager.tsx new file mode 100644 index 0000000..e5abd6e --- /dev/null +++ b/src/components/ActivitiesManager.tsx @@ -0,0 +1,658 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import api from '../lib/axios'; +import { useAuth } from '@/context/AuthContext'; +import { Calendar, User, Building2, Filter, CheckCircle2, Clock, AlertTriangle, RefreshCw, Presentation, FileText, MessageSquare, ListTodo } from 'lucide-react'; + +interface Activity { + id: string; + type: 'FOLLOWUP' | 'DEMO' | 'QUOTE' | 'NEGOTIATION'; + notes: string; + status: string; + date: string; + createdAt: string; + client?: { id: string; name: string; companyName?: string; files?: any[] }; + user?: { id: string; name: string }; + demoPersonName?: string; + demoContactDetails?: string; + keyQueries?: string; + objections?: string; + competitorMention?: string; +} + +interface FilterState { + userId: string; + clientId: string; + dateFrom: string; + dateTo: string; + status: string; + type: string; +} + +export default function ActivitiesManager({ initialClientId, initialOpportunityId }: { initialClientId?: string; initialOpportunityId?: string }) { + const { user } = useAuth(); + const [activities, setActivities] = useState([]); + const [users, setUsers] = useState([]); + const [clients, setClients] = useState([]); + const [opportunities, setOpportunities] = useState([]); + const [loading, setLoading] = useState(false); + const [reassigning, setReassigning] = useState(null); + const [reassignUserId, setReassignUserId] = useState(''); + const [filters, setFilters] = useState({ + userId: '', clientId: initialClientId || '', dateFrom: '', dateTo: '', status: '', type: '' + }); + const [feedbackActivity, setFeedbackActivity] = useState(null); + const [demoFeedback, setDemoFeedback] = useState({ + demoPersonName: '', + demoContactDetails: '', + keyQueries: '', + competitorMention: '' + }); + const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); + const [newActivity, setNewActivity] = useState({ + userId: user?.id || '', + clientId: initialClientId || '', + opportunityId: initialOpportunityId || '', + type: 'FOLLOWUP' as Activity['type'], + notes: '', + date: new Date().toISOString().split('T')[0], + time: '10:00', + demoPersonName: '', + demoContactDetails: '', + keyQueries: '', + objections: '', + competitorMention: '' + }); + + useEffect(() => { + if (user?.id && !newActivity.userId) { + setNewActivity(prev => ({ ...prev, userId: user.id })); + } + }, [user, newActivity.userId]); + + const isAdminOrGM = ['ADMIN', 'GENERAL_MANAGER'].includes(user?.role || ''); + + useEffect(() => { + fetchActivities(); + api.get('/users').then(r => setUsers(r.data)).catch(() => {}); + api.get('/clients').then(r => setClients(r.data)).catch(() => {}); + api.get('/opportunities').then(r => setOpportunities(r.data)).catch(() => {}); + }, []); + + const fetchActivities = async (f: FilterState = filters) => { + setLoading(true); + try { + const params = new URLSearchParams(); + if (f.userId) params.append('userId', f.userId); + if (f.clientId) params.append('clientId', f.clientId); + if (f.dateFrom) params.append('dateFrom', f.dateFrom); + if (f.dateTo) params.append('dateTo', f.dateTo); + if (f.status) params.append('status', f.status); + if (f.type) params.append('type', f.type); + const res = await api.get(`/followups?${params.toString()}`); + setActivities(res.data); + } catch (e) { + console.error(e); + } finally { + setLoading(false); + } + }; + + const handleFilterChange = (key: keyof FilterState, value: string) => { + const updated = { ...filters, [key]: value }; + setFilters(updated); + }; + + const handleApply = () => fetchActivities(filters); + + const handleReset = () => { + const reset: FilterState = { userId: '', clientId: '', dateFrom: '', dateTo: '', status: '', type: '' }; + setFilters(reset); + fetchActivities(reset); + }; + + const handleMarkDone = async (activity: Activity) => { + if (activity.type === 'DEMO') { + setFeedbackActivity(activity); + setDemoFeedback({ demoPersonName: '', demoContactDetails: '', keyQueries: '', competitorMention: '' }); + return; + } + + if (!window.confirm('Mark this activity as DONE?')) return; + try { + await api.patch(`/followups/${activity.id}`, { status: 'DONE' }); + setActivities(activities.map(a => a.id === activity.id ? { ...a, status: 'DONE' } : a)); + } catch (e) { + alert('Failed to update status.'); + } + }; + + const submitDemoFeedback = async (e: React.FormEvent) => { + e.preventDefault(); + if (!feedbackActivity) return; + + if (!demoFeedback.demoPersonName || !demoFeedback.demoContactDetails) { + alert('Please provide Person Met and Contact Details.'); + return; + } + + try { + await api.patch(`/followups/${feedbackActivity.id}`, { + status: 'DONE', + ...demoFeedback + }); + setActivities(activities.map(a => a.id === feedbackActivity.id ? { ...a, status: 'DONE', ...demoFeedback } : a)); + setFeedbackActivity(null); + } catch (e) { + alert('Failed to submit demo feedback.'); + } + }; + + const handleReassign = async (id: string) => { + if (!reassignUserId) { alert('Please select a user to reassign to.'); return; } + try { + await api.patch(`/followups/${id}`, { userId: reassignUserId }); + setReassigning(null); + setReassignUserId(''); + fetchActivities(filters); + } catch (e) { + alert('Failed to reassign task.'); + } + }; + + const handleCreateSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!newActivity.clientId || !newActivity.userId || !newActivity.date) { + alert('Please fill in all required fields.'); + return; + } + + try { + const dateStr = `${newActivity.date}T${newActivity.time}:00`; + await api.post('/followups', { + ...newActivity, + opportunityId: newActivity.opportunityId || null, + date: new Date(dateStr).toISOString(), + status: 'PENDING' + }); + setIsCreateModalOpen(false); + setNewActivity({ + userId: user?.id || '', + clientId: initialClientId || '', + opportunityId: initialOpportunityId || '', + type: 'FOLLOWUP', + notes: '', + date: new Date().toISOString().split('T')[0], + time: '10:00', + demoPersonName: '', + demoContactDetails: '', + keyQueries: '', + objections: '', + competitorMention: '' + }); + fetchActivities(filters); + alert('Activity scheduled successfully!'); + } catch (e) { + alert('Failed to create activity.'); + } + }; + + const groupByDate = (items: Activity[]) => { + const map: Record = {}; + items.forEach(a => { + const key = new Date(a.date).toLocaleDateString('en-IN', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }); + if (!map[key]) map[key] = []; + map[key].push(a); + }); + return Object.entries(map); + }; + + const today = new Date(); today.setHours(0, 0, 0, 0); + + const stats = { + total: activities.length, + pending: activities.filter(a => a.status === 'PENDING').length, + overdue: activities.filter(a => a.status === 'PENDING' && new Date(a.date) < today).length, + done: activities.filter(a => a.status === 'DONE').length, + }; + + const getTypeIcon = (type: Activity['type']) => { + switch(type) { + case 'DEMO': return ; + case 'QUOTE': return ; + case 'NEGOTIATION': return ; + default: return ; + } + }; + + const getTypeBadge = (type: Activity['type']) => { + const styles: Record = { + DEMO: 'bg-blue-100 text-blue-700 border-blue-200', + QUOTE: 'bg-purple-100 text-purple-700 border-purple-200', + NEGOTIATION: 'bg-amber-100 text-amber-700 border-amber-200', + FOLLOWUP: 'bg-indigo-100 text-indigo-700 border-indigo-200', + }; + return ( + + {type} + + ); + }; + + return ( +
+ {/* Header */} +
+
+

Activity Manager

+

Track and manage all scheduled pipeline activities

+
+ +
+ + {/* Stats Row */} +
+ {[ + { label: 'Total', value: stats.total, icon: , color: 'text-gray-600', bg: 'bg-gray-50' }, + { label: 'Pending', value: stats.pending, icon: , color: 'text-amber-600', bg: 'bg-amber-50' }, + { label: 'Overdue', value: stats.overdue, icon: , color: 'text-red-600', bg: 'bg-red-50' }, + { label: 'Done', value: stats.done, icon: , color: 'text-emerald-600', bg: 'bg-emerald-50' }, + ].map(s => ( +
+
{s.icon}
+
+
{s.value}
+
{s.label}
+
+
+ ))} +
+ + {/* Filters */} +
+
+
+ + +
+ {!initialClientId && ( +
+ + +
+ )} +
+ + handleFilterChange('dateFrom', e.target.value)} + className="w-full p-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-odoo-primary outline-none bg-white" + /> +
+
+ + handleFilterChange('dateTo', e.target.value)} + className="w-full p-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-odoo-primary outline-none bg-white" + /> +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + {/* Timeline View */} +
+ {loading ? ( +
Loading activities...
+ ) : activities.length === 0 ? ( +
+

📭

+

No activities match these filters.

+
+ ) : ( + groupByDate(activities).map(([dateLabel, items]) => { + const dateObj = new Date(items[0].date); + dateObj.setHours(0,0,0,0); + const isToday = dateObj.getTime() === today.getTime(); + const isPast = dateObj < today; + return ( +
+
+
+ {isToday ? '📅 Today' : isPast ? `⚠️ ${dateLabel}` : dateLabel} +
+
+ {items.length} item{items.length !== 1 ? 's' : ''} +
+
+ {items.map(a => ( +
+
+ {getTypeIcon(a.type)} +
+
+
+ {a.client && {a.client.companyName || a.client.name}} + {getTypeBadge(a.type)} + {a.user && isAdminOrGM && Assigned to {a.user.name}} +
+

{a.notes}

+ {a.type === 'DEMO' && a.demoPersonName && ( +
+

Met: {a.demoPersonName}

+

Contact: {a.demoContactDetails}

+ {a.competitorMention &&

Competitor: {a.competitorMention}

} +
+ )} +

+ 🕐 {new Date(a.date).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} +

+
+
+ {a.status === 'DONE' ? ( + + Done + + ) : ( + + )} + {isAdminOrGM && a.status !== 'DONE' && ( + reassigning === a.id ? ( +
+ + + +
+ ) : ( + + ) + )} +
+
+ ))} +
+
+ ); + }) + )} +
+ + {/* Create Activity Modal */} + {isCreateModalOpen && ( +
{ + if (e.target === e.currentTarget) setIsCreateModalOpen(false); + }} + > +
+
+

Schedule Pipeline Activity

+ +
+
+
+
+ +
+ {[ + { id: 'FOLLOWUP', label: 'Follow-up', icon: , color: 'indigo' }, + { id: 'DEMO', label: 'Demo', icon: , color: 'blue' }, + { id: 'QUOTE', label: 'Quote', icon: , color: 'purple' }, + { id: 'NEGOTIATION', label: 'Negotiate', icon: , color: 'amber' }, + ].map(t => ( + + ))} +
+
+ +
+ + {newActivity.type === 'QUOTE' ? ( + + ) : ( + + )} +
+ +
+ + +
+ +
+ + setNewActivity({ ...newActivity, date: e.target.value })} + className="w-full p-3 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:ring-2 focus:ring-odoo-primary" + /> +
+
+ + setNewActivity({ ...newActivity, time: e.target.value })} + className="w-full p-3 bg-gray-50 border border-gray-200 rounded-xl outline-none focus:ring-2 focus:ring-odoo-primary" + /> +
+
+ + {/* Demo details moved to Opportunity update modal, removed from scheduling */} + +
+ +