diff --git a/next.config.ts b/next.config.ts index e9ffa30..9ab38a3 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,7 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + reactStrictMode: false, }; export default nextConfig; diff --git a/package.json b/package.json index 22c14a4..cd6863c 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,9 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev -p 3001", + "dev": "next dev -p 3005", "build": "next build", - "start": "next start -p 3001", + "start": "next start -p 3005", "lint": "eslint" }, "dependencies": { diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index d6be50e..858989f 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -18,7 +18,9 @@ import ActivitiesManager from '@/components/ActivitiesManager'; import FunnelAnalysisPage from '@/components/FunnelAnalysisPage'; import CallLogs from '@/components/CallLogs'; import Settings from '@/components/Settings'; -import { LayoutDashboard, Map, Users, Package, LogOut, Menu, UserPlus, DollarSign, FileText, BarChart, TrendingUp, Briefcase, IndianRupee, Target, CalendarCheck, GitMerge, PhoneCall, Settings as SettingsIcon } from 'lucide-react'; +import FloatingEventButton from '@/components/FloatingEventButton'; +import AdminCalculations from '@/components/AdminCalculations'; +import { LayoutDashboard, Map, Users, Package, LogOut, Menu, UserPlus, DollarSign, FileText, BarChart, TrendingUp, Briefcase, IndianRupee, Target, CalendarCheck, GitMerge, PhoneCall, Settings as SettingsIcon, Calculator } from 'lucide-react'; import clsx from 'clsx'; const LiveMap = dynamic(() => import('@/components/LiveMap'), { @@ -43,24 +45,31 @@ export default function DashboardPage() { return
Loading...
; } - const menuItems = [ + const userPermissions: string[] = user?.permissions ? JSON.parse(user.permissions) : []; + + const allMenuItems = [ { id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard }, - ...(user?.role === 'ADMIN' || user?.role === 'GENERAL_MANAGER' ? [{ id: 'tracking', label: 'Live Tracking', icon: Map }] : []), + { id: 'tracking', label: 'Live Tracking', icon: Map }, { id: 'opportunities', label: 'Opportunities', icon: Briefcase }, { id: 'clients', label: 'Clients', icon: Users }, { id: 'quotes', label: 'Quotes', icon: FileText }, { id: 'expenses', label: 'Expenses', icon: IndianRupee }, { 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: 'targets', label: 'Targets', icon: Target }, { 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: 'funnel-analysis', label: 'Funnel Analysis', icon: GitMerge }, + { id: 'pipeline-engine', label: 'Pipeline Engine', icon: Calculator }, { id: 'products', label: 'Products', icon: Package }, { id: 'users', label: 'Users', icon: UserPlus }, { id: 'settings', label: 'Settings', icon: SettingsIcon }, ]; + const menuItems = allMenuItems.filter(item => { + return userPermissions.includes(item.id) || user?.role === 'ADMIN'; + }); + const renderContent = () => { switch (activeTab) { case 'dashboard': @@ -108,6 +117,8 @@ export default function DashboardPage() { return ; case 'funnel-analysis': return ; + case 'pipeline-engine': + return (user?.role === 'ADMIN' || userPermissions.includes('pipeline-engine')) ? : ; case 'products': return ; case 'targets': @@ -216,6 +227,9 @@ export default function DashboardPage() {
{renderContent()}
+ + {/* Floating Quick Action */} + diff --git a/src/components/ActivitiesManager.tsx b/src/components/ActivitiesManager.tsx index e5abd6e..5827581 100644 --- a/src/components/ActivitiesManager.tsx +++ b/src/components/ActivitiesManager.tsx @@ -3,22 +3,25 @@ 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'; +import { Calendar, User, Building2, Filter, CheckCircle2, Clock, AlertTriangle, RefreshCw, Presentation, FileText, MessageSquare, ListTodo, Phone, Send, MapPin, Handshake, Mail, MessageCircle, ClipboardCheck, FileSearch } from 'lucide-react'; interface Activity { id: string; - type: 'FOLLOWUP' | 'DEMO' | 'QUOTE' | 'NEGOTIATION'; + type: 'CALL' | 'MESSAGE' | 'DEMO_SCHEDULED' | 'DEMO_COMPLETED' | 'QUOTE_REQUEST' | 'QUOTE_SEND' | 'VISIT_SCHEDULED' | 'VISIT_COMPLETED' | 'NEGOTIATION' | 'FOLLOWUP' | 'DEMO' | 'QUOTE'; notes: string; status: string; date: string; createdAt: string; client?: { id: string; name: string; companyName?: string; files?: any[] }; user?: { id: string; name: string }; + opportunity?: { id: string; title: string }; + enquiry?: { id: string; products?: { name: string }[] }; demoPersonName?: string; demoContactDetails?: string; keyQueries?: string; objections?: string; competitorMention?: string; + stage: 'LEAD' | 'QUALIFIED' | 'POTENTIAL' | 'SALES' | 'CLOSED'; } interface FilterState { @@ -28,9 +31,11 @@ interface FilterState { dateTo: string; status: string; type: string; + opportunityId?: string; + enquiryId?: string; } -export default function ActivitiesManager({ initialClientId, initialOpportunityId }: { initialClientId?: string; initialOpportunityId?: string }) { +export default function ActivitiesManager({ initialClientId, initialOpportunityId, initialEnquiryId }: { initialClientId?: string; initialOpportunityId?: string; initialEnquiryId?: string }) { const { user } = useAuth(); const [activities, setActivities] = useState([]); const [users, setUsers] = useState([]); @@ -40,20 +45,30 @@ export default function ActivitiesManager({ initialClientId, initialOpportunityI const [reassigning, setReassigning] = useState(null); const [reassignUserId, setReassignUserId] = useState(''); const [filters, setFilters] = useState({ - userId: '', clientId: initialClientId || '', dateFrom: '', dateTo: '', status: '', type: '' + userId: '', clientId: initialClientId || '', dateFrom: '', dateTo: '', status: '', type: '', opportunityId: initialOpportunityId || '', enquiryId: initialEnquiryId || '' }); const [feedbackActivity, setFeedbackActivity] = useState(null); const [demoFeedback, setDemoFeedback] = useState({ demoPersonName: '', demoContactDetails: '', keyQueries: '', - competitorMention: '' + competitorMention: '', + customerFeedback: '', + requirementDetails: '', + suggestions: '', + budget: '', + expectedClosingTimeline: '', + competitorInfo: '', + staffRemarks: '', + customerCommitments: '', + caCsDetails: '' }); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [newActivity, setNewActivity] = useState({ userId: user?.id || '', clientId: initialClientId || '', opportunityId: initialOpportunityId || '', + enquiryId: initialEnquiryId || '', type: 'FOLLOWUP' as Activity['type'], notes: '', date: new Date().toISOString().split('T')[0], @@ -62,7 +77,8 @@ export default function ActivitiesManager({ initialClientId, initialOpportunityI demoContactDetails: '', keyQueries: '', objections: '', - competitorMention: '' + competitorMention: '', + stage: 'LEAD' as const }); useEffect(() => { @@ -90,6 +106,8 @@ export default function ActivitiesManager({ initialClientId, initialOpportunityI if (f.dateTo) params.append('dateTo', f.dateTo); if (f.status) params.append('status', f.status); 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); } catch (e) { @@ -107,15 +125,29 @@ export default function ActivitiesManager({ initialClientId, initialOpportunityI const handleApply = () => fetchActivities(filters); const handleReset = () => { - const reset: FilterState = { userId: '', clientId: '', dateFrom: '', dateTo: '', status: '', type: '' }; + const reset: FilterState = { userId: '', clientId: '', dateFrom: '', dateTo: '', status: '', type: '', opportunityId: '', enquiryId: '' }; setFilters(reset); fetchActivities(reset); }; const handleMarkDone = async (activity: Activity) => { - if (activity.type === 'DEMO') { + if (activity.type === 'DEMO' || activity.type === 'DEMO_SCHEDULED' || activity.type === 'VISIT_SCHEDULED' || activity.type === 'DEMO_COMPLETED' || activity.type === 'VISIT_COMPLETED') { setFeedbackActivity(activity); - setDemoFeedback({ demoPersonName: '', demoContactDetails: '', keyQueries: '', competitorMention: '' }); + setDemoFeedback({ + demoPersonName: '', + demoContactDetails: '', + keyQueries: '', + competitorMention: '', + customerFeedback: '', + requirementDetails: '', + suggestions: '', + budget: '', + expectedClosingTimeline: '', + competitorInfo: '', + staffRemarks: '', + customerCommitments: '', + caCsDetails: '' + }); return; } @@ -132,9 +164,25 @@ export default function ActivitiesManager({ initialClientId, initialOpportunityI e.preventDefault(); if (!feedbackActivity) return; - if (!demoFeedback.demoPersonName || !demoFeedback.demoContactDetails) { - alert('Please provide Person Met and Contact Details.'); - return; + const isMandatoryType = ['DEMO_COMPLETED', 'VISIT_COMPLETED', 'DEMO'].includes(feedbackActivity.type); + + if (isMandatoryType) { + const requiredFields = [ + 'customerFeedback', 'requirementDetails', 'budget', + 'expectedClosingTimeline', 'competitorInfo', 'staffRemarks', + 'customerCommitments', 'caCsDetails' + ]; + const missing = requiredFields.filter(f => !demoFeedback[f as keyof typeof demoFeedback]); + if (missing.length > 0) { + alert(`Please fill all mandatory fields: ${missing.join(', ')}`); + return; + } + } else { + // For other types (like scheduled ones marked done), person name is still helpful + if (!demoFeedback.demoPersonName || !demoFeedback.demoContactDetails) { + alert('Please provide Person Met and Contact Details.'); + return; + } } try { @@ -173,6 +221,7 @@ export default function ActivitiesManager({ initialClientId, initialOpportunityI await api.post('/followups', { ...newActivity, opportunityId: newActivity.opportunityId || null, + enquiryId: newActivity.enquiryId || null, date: new Date(dateStr).toISOString(), status: 'PENDING' }); @@ -181,6 +230,7 @@ export default function ActivitiesManager({ initialClientId, initialOpportunityI userId: user?.id || '', clientId: initialClientId || '', opportunityId: initialOpportunityId || '', + enquiryId: initialEnquiryId || '', type: 'FOLLOWUP', notes: '', date: new Date().toISOString().split('T')[0], @@ -189,7 +239,8 @@ export default function ActivitiesManager({ initialClientId, initialOpportunityI demoContactDetails: '', keyQueries: '', objections: '', - competitorMention: '' + competitorMention: '', + stage: 'LEAD' }); fetchActivities(filters); alert('Activity scheduled successfully!'); @@ -219,23 +270,39 @@ export default function ActivitiesManager({ initialClientId, initialOpportunityI const getTypeIcon = (type: Activity['type']) => { switch(type) { + case 'CALL': return ; + case 'MESSAGE': return ; + case 'DEMO_SCHEDULED': return ; + case 'DEMO_COMPLETED': return ; + case 'QUOTE_REQUEST': return ; + case 'QUOTE_SEND': return ; + case 'VISIT_SCHEDULED': return ; + case 'VISIT_COMPLETED': return ; + case 'NEGOTIATION': return ; case 'DEMO': return ; case 'QUOTE': return ; - case 'NEGOTIATION': return ; - default: return ; + default: return ; } }; const getTypeBadge = (type: Activity['type']) => { const styles: Record = { + CALL: 'bg-green-100 text-green-700 border-green-200', + MESSAGE: 'bg-cyan-100 text-cyan-700 border-cyan-200', + DEMO_SCHEDULED: 'bg-blue-100 text-blue-700 border-blue-200', + DEMO_COMPLETED: 'bg-emerald-100 text-emerald-700 border-emerald-200', + QUOTE_REQUEST: 'bg-purple-100 text-purple-700 border-purple-200', + QUOTE_SEND: 'bg-indigo-100 text-indigo-700 border-indigo-200', + VISIT_SCHEDULED: 'bg-orange-100 text-orange-700 border-orange-200', + VISIT_COMPLETED: 'bg-red-100 text-red-700 border-red-200', + NEGOTIATION: 'bg-amber-100 text-amber-700 border-amber-200', + FOLLOWUP: 'bg-slate-100 text-slate-700 border-slate-200', 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} + + {type.replace('_', ' ')} ); }; @@ -383,6 +450,8 @@ export default function ActivitiesManager({ initialClientId, initialOpportunityI
{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}}
@@ -472,19 +541,25 @@ export default function ActivitiesManager({ initialClientId, initialOpportunityI
{[ - { 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' }, + { 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: 'QUOTE_REQUEST', label: 'Quote Req', icon: , color: 'purple' }, + { id: 'QUOTE_SEND', label: 'Quote Send', icon: , color: 'indigo' }, + { id: 'VISIT_SCHEDULED', label: 'Visit Sch', icon: , color: 'orange' }, + { id: 'VISIT_COMPLETED', label: 'Visit Done', icon: , color: 'red' }, + { id: 'NEGOTIATION', label: 'Negotiate', icon: , color: 'amber' }, + { id: 'FOLLOWUP', label: 'Other', icon: , color: 'slate' }, ].map(t => ( ))}
@@ -542,6 +617,22 @@ export default function ActivitiesManager({ initialClientId, initialOpportunityI ))}
+ +
+ + +
@@ -601,50 +692,126 @@ export default function ActivitiesManager({ initialClientId, initialOpportunityI
-

Demo Feedback

+

Activity Feedback

- - setDemoFeedback({ ...demoFeedback, demoPersonName: e.target.value })} - className="w-full p-2.5 bg-white border border-gray-200 rounded-xl outline-none focus:ring-2 focus:ring-odoo-primary text-sm" - placeholder="e.g. John Doe (CTO)" - /> -
-
- - setDemoFeedback({ ...demoFeedback, demoContactDetails: e.target.value })} - className="w-full p-2.5 bg-white border border-gray-200 rounded-xl outline-none focus:ring-2 focus:ring-odoo-primary text-sm" - placeholder="Phone or Email" - /> -
-
- +