diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 858989f..88eaf4f 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -15,6 +15,7 @@ import OpportunityBoard from '@/components/OpportunityBoard'; import TargetManager from '@/components/TargetManager'; import IncentiveManager from '@/components/IncentiveManager'; import ActivitiesManager from '@/components/ActivitiesManager'; +import PipelineActivityEngine from '@/components/PipelineActivityEngine'; import FunnelAnalysisPage from '@/components/FunnelAnalysisPage'; import CallLogs from '@/components/CallLogs'; import Settings from '@/components/Settings'; @@ -112,7 +113,7 @@ export default function DashboardPage() { case 'reports': return ; case 'activities': - return
; + return ; case 'call-logs': return ; case 'funnel-analysis': diff --git a/src/components/ActivitiesManager.tsx b/src/components/ActivitiesManager.tsx index 5827581..a50b6d2 100644 --- a/src/components/ActivitiesManager.tsx +++ b/src/components/ActivitiesManager.tsx @@ -3,11 +3,11 @@ 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, Phone, Send, MapPin, Handshake, Mail, MessageCircle, ClipboardCheck, FileSearch } from 'lucide-react'; +import { Calendar, User, Building2, Filter, CheckCircle2, Clock, AlertTriangle, RefreshCw, Presentation, FileText, MessageSquare, ListTodo, Phone, Send, MapPin, Handshake, Mail, MessageCircle, ClipboardCheck, FileSearch, TrendingUp } from 'lucide-react'; interface Activity { id: string; - type: 'CALL' | 'MESSAGE' | 'DEMO_SCHEDULED' | 'DEMO_COMPLETED' | 'QUOTE_REQUEST' | 'QUOTE_SEND' | 'VISIT_SCHEDULED' | 'VISIT_COMPLETED' | 'NEGOTIATION' | 'FOLLOWUP' | 'DEMO' | 'QUOTE'; + type: 'CALL' | 'MESSAGE' | 'DEMO_SCHEDULED' | 'DEMO_COMPLETED' | 'QUOTE_REQUEST' | 'QUOTE_SEND' | 'VISIT_SCHEDULED' | 'VISIT_COMPLETED' | 'NEGOTIATION' | 'FOLLOWUP' | 'DEMO' | 'QUOTE' | 'SECOND_DEMO' | 'SECOND_QUOTE' | 'MANAGER_HELP' | 'PROBABILITY_UPDATE' | 'TIMEFRAME_UPDATE'; notes: string; status: string; date: string; @@ -35,7 +35,7 @@ interface FilterState { enquiryId?: string; } -export default function ActivitiesManager({ initialClientId, initialOpportunityId, initialEnquiryId }: { initialClientId?: string; initialOpportunityId?: string; initialEnquiryId?: string }) { +export default function ActivitiesManager({ initialClientId, initialOpportunityId, initialEnquiryId, initialActivityId }: { initialClientId?: string; initialOpportunityId?: string; initialEnquiryId?: string; initialActivityId?: string }) { const { user } = useAuth(); const [activities, setActivities] = useState([]); const [users, setUsers] = useState([]); @@ -48,6 +48,7 @@ export default function ActivitiesManager({ initialClientId, initialOpportunityI userId: '', clientId: initialClientId || '', dateFrom: '', dateTo: '', status: '', type: '', opportunityId: initialOpportunityId || '', enquiryId: initialEnquiryId || '' }); const [feedbackActivity, setFeedbackActivity] = useState(null); + const [stageChangedMsg, setStageChangedMsg] = useState(null); const [demoFeedback, setDemoFeedback] = useState({ demoPersonName: '', demoContactDetails: '', @@ -108,10 +109,14 @@ export default function ActivitiesManager({ initialClientId, initialOpportunityI if (f.type) params.append('type', f.type); if (f.opportunityId) params.append('opportunityId', f.opportunityId); if (f.enquiryId) params.append('enquiryId', f.enquiryId); - const res = await api.get(`/followups?${params.toString()}`); - setActivities(res.data); + const res = await api.get('/followups', { params }); + let data = res.data; + if (initialActivityId) { + data = data.filter((a: Activity) => a.id === initialActivityId); + } + setActivities(data); } catch (e) { - console.error(e); + console.error('Failed to load activities', e); } finally { setLoading(false); } @@ -170,7 +175,7 @@ export default function ActivitiesManager({ initialClientId, initialOpportunityI const requiredFields = [ 'customerFeedback', 'requirementDetails', 'budget', 'expectedClosingTimeline', 'competitorInfo', 'staffRemarks', - 'customerCommitments', 'caCsDetails' + 'customerCommitments', 'caCsDetails', 'suggestions' ]; const missing = requiredFields.filter(f => !demoFeedback[f as keyof typeof demoFeedback]); if (missing.length > 0) { @@ -178,7 +183,7 @@ export default function ActivitiesManager({ initialClientId, initialOpportunityI return; } } else { - // For other types (like scheduled ones marked done), person name is still helpful + // For scheduled types marked done — person name is still helpful if (!demoFeedback.demoPersonName || !demoFeedback.demoContactDetails) { alert('Please provide Person Met and Contact Details.'); return; @@ -192,6 +197,16 @@ export default function ActivitiesManager({ initialClientId, initialOpportunityI }); setActivities(activities.map(a => a.id === feedbackActivity.id ? { ...a, status: 'DONE', ...demoFeedback } : a)); setFeedbackActivity(null); + + // Determine expected stage change for user feedback + const isVisit = ['VISIT_SCHEDULED', 'VISIT_COMPLETED'].includes(feedbackActivity.type); + const isDemo = ['DEMO', 'DEMO_SCHEDULED', 'DEMO_COMPLETED'].includes(feedbackActivity.type); + if (isVisit) { + setStageChangedMsg('✅ Activity marked done! Pipeline stage auto-advanced to SALES.'); + } else if (isDemo) { + setStageChangedMsg('✅ Demo marked done! Pipeline stage auto-advanced to POTENTIAL.'); + } + setTimeout(() => setStageChangedMsg(null), 4000); } catch (e) { alert('Failed to submit demo feedback.'); } @@ -308,7 +323,14 @@ export default function ActivitiesManager({ initialClientId, initialOpportunityI }; return ( -
+
+ {/* Stage Change Toast */} + {stageChangedMsg && ( +
+ {stageChangedMsg} + +
+ )} {/* Header */}
@@ -417,108 +439,167 @@ export default function ActivitiesManager({ initialClientId, initialOpportunityI
- {/* Timeline View */} -
+ {/* Table 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}} - {a.opportunity && {a.opportunity.title}} - {a.enquiry && 0 ? a.enquiry.products.map((p: any) => p.name).join(', ') : 'Enquiry'}>{a.enquiry.products && a.enquiry.products.length > 0 ? a.enquiry.products.map((p: any) => p.name).join(', ') : 'Enquiry'}} - {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 ? ( -
- - - -
- ) : ( - - ) - )} -
+
+ + + + + + + + + + + {/* New Activity Quick Add Trigger Row */} + setIsCreateModalOpen(true)} + className="bg-orange-50/40 hover:bg-orange-50 cursor-pointer border-b-2 border-orange-100 group transition-all" + > + + + + {activities.length === 0 ? ( + + + + ) : ( + (() => { + const sortedActivities = [...activities].sort((a, b) => { + const dateA = new Date(a.date); + const dateB = new Date(b.date); + const isOverdueA = a.status === 'PENDING' && dateA < today; + const isOverdueB = b.status === 'PENDING' && dateB < today; + + if (isOverdueA && !isOverdueB) return -1; + if (!isOverdueA && isOverdueB) return 1; + + const isPendingA = a.status === 'PENDING'; + const isPendingB = b.status === 'PENDING'; + if (isPendingA && !isPendingB) return -1; + if (!isPendingA && isPendingB) return 1; + + if (isPendingA && isPendingB) return dateA.getTime() - dateB.getTime(); + return dateB.getTime() - dateA.getTime(); + }); + + return sortedActivities.map(a => { + const dateObj = new Date(a.date); + const isOverdue = a.status === 'PENDING' && dateObj < today; + const isPending = a.status === 'PENDING' && !isOverdue; + const isDone = a.status === 'DONE'; + + let rowColor = "bg-white hover:bg-gray-50"; + let textColor = "text-gray-800"; + if (isOverdue) { + rowColor = "bg-red-50 hover:bg-red-100/50"; + textColor = "text-red-900"; + } else if (isPending) { + rowColor = "bg-orange-50 hover:bg-orange-100/50"; + textColor = "text-orange-900"; + } else if (isDone) { + rowColor = "bg-emerald-50/60 hover:bg-emerald-50"; + textColor = "text-emerald-900"; + } + + return ( + + + + + + + + + + ); + }); + })() + )} + +
Date & TimeActivityStaffRemarks
+
+
+
+ Click here to add new activity
- ))} - - - ); - }) +
+

📭

+

No activities match these filters.

+
+
+ {dateObj.toLocaleDateString('en-IN', { day: '2-digit', month: '2-digit', year: 'numeric' })} +
+
+ {dateObj.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} +
+
+
+
+ {getTypeIcon(a.type)} + {a.type.replace('_', ' ')} +
+ {a.status === 'DONE' && ( + Done + )} +
+ {a.client &&
🏢 {a.client.companyName || a.client.name}
} + {a.opportunity &&
💼 {a.opportunity.title}
} +
+
+ + {a.user?.name || 'Unassigned'} +
+ {isAdminOrGM && a.status !== 'DONE' && ( +
+ {reassigning === a.id ? ( +
+ + + +
+ ) : ( + + )} +
+ )} +
+
+

+ {a.notes || No remarks} +

+ {a.type === 'DEMO' && a.demoPersonName && ( +
+ Met: {a.demoPersonName} ({a.demoContactDetails}) +
+ )} + {a.status !== 'DONE' && ( +
+ +
+ )} +
+
+
)}
@@ -539,16 +620,21 @@ export default function ActivitiesManager({ initialClientId, initialOpportunityI
-
+
{[ { id: 'CALL', label: 'Call', icon: , color: 'green' }, { id: 'MESSAGE', label: 'Msg', icon: , color: 'cyan' }, { id: 'DEMO_SCHEDULED', label: 'Demo Sch', icon: , color: 'blue' }, { id: 'DEMO_COMPLETED', label: 'Demo Done', icon: , color: 'emerald' }, + { id: 'SECOND_DEMO', label: '2nd Demo', icon: , color: 'teal' }, { id: 'QUOTE_REQUEST', label: 'Quote Req', icon: , color: 'purple' }, { id: 'QUOTE_SEND', label: 'Quote Send', icon: , color: 'indigo' }, + { id: 'SECOND_QUOTE', label: '2nd Quote', icon: , color: 'violet' }, { id: 'VISIT_SCHEDULED', label: 'Visit Sch', icon: , color: 'orange' }, { id: 'VISIT_COMPLETED', label: 'Visit Done', icon: , color: 'red' }, + { id: 'MANAGER_HELP', label: 'Mgr Help', icon: , color: 'rose' }, + { id: 'PROBABILITY_UPDATE', label: 'Prob Update', icon: , color: 'fuchsia' }, + { id: 'TIMEFRAME_UPDATE', label: 'Time Update', icon: , color: 'sky' }, { id: 'NEGOTIATION', label: 'Negotiate', icon: , color: 'amber' }, { id: 'FOLLOWUP', label: 'Other', icon: , color: 'slate' }, ].map(t => ( @@ -559,17 +645,33 @@ export default function ActivitiesManager({ initialClientId, initialOpportunityI className={`flex flex-col items-center justify-center p-2 rounded-xl border-2 transition-all ${newActivity.type === t.id ? `border-${t.color}-500 bg-${t.color}-50 text-${t.color}-700` : 'border-gray-100 bg-gray-50 text-gray-500 hover:border-gray-200'}`} > {t.icon} - {t.label} + {t.label} ))}
-
- - {newActivity.type === 'QUOTE' ? ( + {/* Client selector — shown for all types except QUOTE (which links via opportunity) */} + {newActivity.type !== 'QUOTE' && ( +
+ + +
+ )} + + {/* Opportunity selector — required for QUOTE, optional for demo/visit/quote types */} + {newActivity.type === 'QUOTE' ? ( +
+ - ) : ( - setNewActivity({ ...newActivity, opportunityId: 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" > - - {clients.map(c => )} + + {opportunities + .filter(o => !newActivity.clientId || o.clientId === newActivity.clientId) + .map(o => ( + + )) + } - )} -
+
+ )}
@@ -696,7 +806,7 @@ export default function ActivitiesManager({ initialClientId, initialOpportunityI
-
+