From 3bb4c35defa53f7897871929da84e66976502e21 Mon Sep 17 00:00:00 2001 From: Manu Date: Sat, 9 May 2026 15:21:07 +0530 Subject: [PATCH] changes till 09/05/2026 New clients creation from opportunities, client conversion%, time taken for conversion, close the modal when touched outside it Client and company name separate, Demo becomes a separate activity, all changes done in mobile app as well 2) transfer of clients, demos followups negotiation etc scheduling, quote opportunity in place of enquiry, In opportunity new product add, existing dropdown, added option for adding documents on client creation and showing it --- src/app/dashboard/page.tsx | 8 +- src/components/ActivitiesManager.tsx | 658 +++++++++++++++++++++++++++ src/components/ClientList.tsx | 99 +++- src/components/ClientModal.tsx | 409 +++++++++++++---- src/components/FollowupsManager.tsx | 426 ----------------- src/components/OpportunityBoard.tsx | 449 ++++++++++++++---- src/components/QuoteManager.tsx | 49 +- 7 files changed, 1437 insertions(+), 661 deletions(-) create mode 100644 src/components/ActivitiesManager.tsx delete mode 100644 src/components/FollowupsManager.tsx 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 */} + +
+ +