diff --git a/src/components/DashboardOverview.tsx b/src/components/DashboardOverview.tsx index 601e9bc..cbeee2a 100644 --- a/src/components/DashboardOverview.tsx +++ b/src/components/DashboardOverview.tsx @@ -1,117 +1,14 @@ 'use client'; -import React, { useEffect, useState } from 'react'; -import { - LayoutDashboard, - TrendingUp, - Briefcase, - CheckCircle2, - Clock, - ArrowUpRight, - ArrowDownRight, - Search, - Filter, - ArrowRight, - IndianRupee, - AlertCircle -} from 'lucide-react'; -import { - Chart as ChartJS, - CategoryScale, - LinearScale, - BarElement, - Title, - Tooltip, - Legend, - ArcElement, - PointElement, - LineElement, - Filler -} from 'chart.js'; -import { Bar, Pie, Line } from 'react-chartjs-2'; -import api from '../lib/axios'; +import React from 'react'; import { useAuth } from '@/context/AuthContext'; -import { formatDistanceToNow } from 'date-fns'; -import FunnelAnalytics from './FunnelAnalytics'; -import TeamPerformance from './TeamPerformance'; - -ChartJS.register( - CategoryScale, - LinearScale, - BarElement, - Title, - Tooltip, - Legend, - ArcElement, - PointElement, - LineElement, - Filler -); - -interface DashboardStats { - kpis: { - enquiriesToday: number; - pipelineValue: number; - pipelineCount: number; - monthlyRevenue: number; - contributionRevenue: number; - conversionRate: number; - pendingExpenses: number; - }; - performance: { - score: number; - tag: string; - breakdown: { - revenue: number; - conversion: number; - activity: number; - discipline: number; - quality: number; - }; - } | null; - target: { - monthly: number; - minimum: number; - weekly: number; - dailyLead: number; - achieved: number; - requiredLeads?: number; - requiredDemos?: number; - } | null; - recentActivity: { - enquiries: any[]; - opportunities: any[]; - }; - quarterly: { - status: 'NORMAL' | 'WARNING' | 'ACTION'; - suggestions: string[]; - recentScores: { date: string, score: number }[]; - } | null; -} +import MyDashboard from './MyDashboard'; +import ManagerDashboard from './ManagerDashboard'; export default function DashboardOverview() { const { user } = useAuth(); - const [stats, setStats] = useState(null); - const [loading, setLoading] = useState(true); - useEffect(() => { - const fetchDashboardData = async () => { - try { - const [statsRes, quarterlyRes] = await Promise.all([ - api.get('/dashboard/stats'), - api.get(`/performance/quarterly/${user?.id}`) - ]); - setStats({ ...statsRes.data, quarterly: quarterlyRes.data }); - } catch (error) { - console.error('Failed to fetch dashboard stats', error); - } finally { - setLoading(false); - } - }; - if (user?.id) fetchDashboardData(); - }, [user?.id]); - - if (loading || !stats) { + if (!user) { return (
@@ -119,373 +16,7 @@ export default function DashboardOverview() { ); } - const { kpis, recentActivity, performance, target, quarterly } = stats; + const isManager = ['ADMIN', 'GENERAL_MANAGER', 'MANAGER'].includes(user.role); - const kpiCards = [ - { - label: "Performance Score", - value: performance ? `${Math.round(performance.score)}/100` : 'N/A', - subValue: performance?.tag.replace('_', ' '), - icon: CheckCircle2, - color: performance?.score && performance.score > 80 ? 'bg-emerald-500' : performance?.score && performance.score > 50 ? 'bg-amber-500' : 'bg-rose-500', - trend: performance?.score && performance.score > 80 ? 'EXCELLENT' : 'KEEP GOING', - trendUp: true - }, - { - label: "Open Pipeline", - value: `₹${(kpis.pipelineValue / 100000).toFixed(1)}L`, - subValue: `${kpis.pipelineCount} deals`, - icon: Briefcase, - color: 'bg-odoo-primary', - trend: '+5.4', - trendUp: true - }, - { - label: "Contribution (MTD)", - value: `₹${(kpis.contributionRevenue / 1000).toFixed(1)}k`, - subValue: `of ₹${(kpis.monthlyRevenue / 1000).toFixed(1)}k total`, - icon: TrendingUp, - color: 'bg-indigo-500', - trend: '50/50 SPLIT', - trendUp: true - }, - { - label: "Target Gap", - value: target ? `₹${((target.monthly - target.achieved) / 1000).toFixed(1)}k` : 'N/A', - subValue: target ? `${Math.round((target.achieved / target.monthly) * 100)}% reached` : 'No Target', - icon: IndianRupee, - color: 'bg-emerald-500', - trend: target && target.achieved >= target.minimum ? 'QUALIFIED' : 'PENDING', - trendUp: target && target.achieved >= target.minimum - } - ]; - - const pipelineChartData = { - labels: ['Lead', 'Qualified', 'Potential', 'SALES'], - datasets: [ - { - label: 'Deals', - data: [15, 8, 5, 3, 2], // Placeholder for visual richness - backgroundColor: [ - 'rgba(113, 75, 103, 0.7)', - 'rgba(113, 75, 103, 0.5)', - 'rgba(0, 160, 157, 0.5)', - 'rgba(0, 160, 157, 0.7)', - 'rgba(16, 185, 129, 0.8)', - ], - borderRadius: 8, - }, - ], - }; - - return ( -
- {/* Header / Welcome */} -
-
-

- Dashboard -

-

- Welcome back, {user?.name}. Here's what's happening today. -

-
-
- - -
-
- - {/* KPI Cards */} -
- {kpiCards.map((card, i) => ( -
-
- -
-
- -
-
- {card.trendUp ? : } - {card.trend} -
-
- -
-

{card.value}

-

{card.label}

- {card.subValue && {card.subValue}} -
-
- ))} -
- -
- {/* Main Stats Area */} -
- {/* Performance Warnings (Quarterly Logic) - ADMIN ONLY */} - {(user?.role === 'ADMIN' || user?.role === 'GENERAL_MANAGER') && quarterly && quarterly.status !== 'NORMAL' && ( -
-
-
- -
-
-

- Performance {quarterly.status} -

-

- {quarterly.status === 'ACTION' - ? "Immediate improvement required to meet organizational standards." - : "Performance has been below minimum for 2 months. Attention required." - } -

-
-
-
- {quarterly.status} -
-
- )} - - {/* Team Performance Leaderboard - ADMIN ONLY */} - {(user?.role === 'ADMIN' || user?.role === 'GENERAL_MANAGER') && ( - - )} - - {/* Efficiency Funnel */} - - - {/* Improvement Suggestions - ADMIN ONLY */} - {(user?.role === 'ADMIN' || user?.role === 'GENERAL_MANAGER') && quarterly && quarterly.suggestions.length > 0 && ( -
-

- - Performance Improvement Plan -

-
- {quarterly.suggestions.map((suggestion, idx) => ( -
-
-

{suggestion}

-
- ))} -
-
- )} - - {/* Pending Actions (Conditional) */} - {(user?.role === 'ADMIN' || user?.role === 'MANAGER') && kpis.pendingExpenses > 0 && ( -
-
-
- -
-
-

Attention Required

-

You have {kpis.pendingExpenses} pending expense claims to approve.

-
-
- -
- )} - - {/* Recent Enquiries List */} -
-
-

Latest Enquiries

- -
-
- {recentActivity.enquiries.map((enq, index) => ( -
-
-
- {enq.client?.name?.charAt(0) || 'E'} -
-
-

{enq.client?.name || 'Quick Enquiry'}

-

{enq.user?.name} • {formatDistanceToNow(new Date(enq.createdAt), { addSuffix: true })}

-
-
-
-
-

New Lead

-

Inbound

-
-
- -
-
-
- ))} -
-
-
- - {/* Sidebar Activity */} -
- {/* Target Achievement Widget */} -
-

Target Achievement

- {target ? ( -
-
-
- MONTHLY TARGET - ₹{(target.monthly / 1000).toFixed(0)}k -
-
-
-
-

Achieved: ₹{(target.achieved / 1000).toFixed(1)}k

-
- -
-
- MINIMUM TARGET - ₹{(target.minimum / 1000).toFixed(0)}k -
-
-
-
- {target.achieved >= target.minimum ? ( -

- MINIMUM REACHED -

- ) : ( -

₹{((target.minimum - target.achieved)/1000).toFixed(1)}k to go

- )} -
- - {/* Benchmarks */} -
-
-

Daily Leads

-

{target.dailyLead}

-
-
-

Req. Demos

-

{target.requiredDemos || Math.ceil(target.monthly / 40000) * 3}

-
-
-
- ) : ( -
- -

NO TARGET SET

-
- )} -
- - {/* Performance Breakdown */} -
-

Performance Mix

- {performance ? ( -
- {[ - { label: 'Revenue', score: performance.breakdown.revenue, max: 40, color: 'bg-emerald-500' }, - { label: 'Conversion', score: performance.breakdown.conversion, max: 20, color: 'bg-odoo-secondary' }, - { label: 'Activity', score: performance.breakdown.activity, max: 15, color: 'bg-indigo-500' }, - { label: 'Discipline', score: performance.breakdown.discipline, max: 15, color: 'bg-amber-500' }, - { label: 'Data Quality', score: performance.breakdown.quality, max: 10, color: 'bg-rose-500' }, - ].map((item, idx) => ( -
-
- {item.label} - {Math.round(item.score)}/{item.max} -
-
-
-
-
- ))} -
- ) : ( -

No data available

- )} -
- - {/* Marketing Impact Breakdown */} -
-

- - Marketing Impact -

-
- {[ - { label: 'WhatsApp', count: 12, color: 'text-emerald-600', bg: 'bg-emerald-50' }, - { label: 'Posters', count: 45, color: 'text-indigo-600', bg: 'bg-indigo-50' }, - { label: 'Exhibitions', count: 2, color: 'text-amber-600', bg: 'bg-amber-50' }, - ].map((act, i) => ( -
- {act.label} - {act.count} -
- ))} -
-

View Detailed Reports

-
- - {/* Recent Opportunities Updates */} -
-
-

Pipeline Pulse

-
-
- {recentActivity.opportunities.map((op, index) => ( -
- {index !== recentActivity.opportunities.length - 1 && ( -
- )} -
-
-

- {op.title} -

-

- Updated to {op.stage} • {formatDistanceToNow(new Date(op.updatedAt))} ago -

-
- ₹{op.value.toLocaleString()} -
-
-
- ))} -
-
- - {/* System Guard Extra Widget */} -
-
-

Growth Index

-

Your productivity is up by 14% this week. Keep tracking your leads!

-
- - Level Up -
-
-
-
-
- ); + return isManager ? : ; } diff --git a/src/components/ManagerDashboard.tsx b/src/components/ManagerDashboard.tsx new file mode 100644 index 0000000..1f62e26 --- /dev/null +++ b/src/components/ManagerDashboard.tsx @@ -0,0 +1,752 @@ +'use client'; + +import React, { useEffect, useState } from 'react'; +import { + IndianRupee, + TrendingUp, + CheckCircle2, + AlertCircle, + Briefcase, + Award, + Users, + Target, + Activity, + Calendar, + ArrowUpRight, + TrendingDown, + Sparkles, + CheckCircle +} from 'lucide-react'; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + BarElement, + Title, + Tooltip, + Legend +} from 'chart.js'; +import { Bar } from 'react-chartjs-2'; +import api from '../lib/axios'; +import { useAuth } from '@/context/AuthContext'; + +ChartJS.register( + CategoryScale, + LinearScale, + BarElement, + Title, + Tooltip, + Legend +); + +interface TeamMemberPerformance { + id: string; + name: string; + role: string; + score: number; + tag: string; + revenueScore: number; + activityScore: number; +} + +interface PipelineHealthItem { + stage: string; + count: number; + value: number; +} + +interface ConversionRatioDetails { + target: number; + revenue: number; + closures: number; + ratio: number; + enquiries: number; + conversionRate: number; +} + +interface ConversionRatioGroup { + thisMonth: ConversionRatioDetails; + lastMonth: ConversionRatioDetails; + quarter: ConversionRatioDetails; +} + +interface ActivityAnalysisItem { + type: string; + scheduled: number; + completed: number; + completionRate: number; +} + +interface PerformanceWeightageItem { + id: string; + name: string; + weightedSales: number; + weightedActivity: number; + totalWeighted: number; +} + +interface PerformerItem { + id: string; + name: string; + role: string; + score: number; +} + +interface PerformerPeriod { + top: PerformerItem[]; + under: PerformerItem[]; +} + +interface PerformersGroup { + month: PerformerPeriod; + quarter: PerformerPeriod; +} + +interface LeadVsSharedDetails { + count: number; + value: number; + conversionRate: number; +} + +interface LeadVsSharedGroup { + own: LeadVsSharedDetails; + shared: LeadVsSharedDetails; + totalEnquiries: number; +} + +interface RevenueContributionItem { + value: number; + leadOwner: string; + closingOwner: string; + isOwn: boolean; + leadOwnerShare: number; + closingOwnerShare: number; +} + +interface ManagerDashboardData { + teamPerformance: TeamMemberPerformance[]; + pipelineHealth: PipelineHealthItem[]; + conversionRatio: ConversionRatioGroup; + activitiesAnalysis: ActivityAnalysisItem[]; + strategicByType: Record; + performanceWeightage: PerformanceWeightageItem[]; + performers: PerformersGroup; + leadVsShared: LeadVsSharedGroup; + revenueContribution: RevenueContributionItem[]; +} + +export default function ManagerDashboard() { + const { user } = useAuth(); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [performerPeriod, setPerformerPeriod] = useState<'month' | 'quarter'>('month'); + + useEffect(() => { + const fetchManagerDashboard = async () => { + try { + setLoading(true); + const res = await api.get('/dashboard/manager'); + setData(res.data); + } catch (err: any) { + console.error("Failed to load Manager Dashboard data", err); + setError(err.response?.data?.message || "Could not retrieve manager dashboard data."); + } finally { + setLoading(false); + } + }; + fetchManagerDashboard(); + }, []); + + const formatLakhs = (value: number) => { + return `₹${(value / 100000).toFixed(2)}L`; + }; + + const formatCurrency = (value: number) => { + return new Intl.NumberFormat('en-IN', { + style: 'currency', + currency: 'INR', + maximumFractionDigits: 0 + }).format(value); + }; + + if (loading) { + return ( +
+
+

Assembling team performance statistics...

+
+ ); + } + + if (error || !data) { + return ( +
+ +

Access Denied / Error

+

{error || "You might not have administrative privileges."}

+ +
+ ); + } + + const { + teamPerformance, + pipelineHealth, + conversionRatio, + activitiesAnalysis, + performanceWeightage, + performers, + leadVsShared, + revenueContribution + } = data; + + // Pipeline Health Chart Data + const pipelineChartData = { + labels: pipelineHealth.map(p => p.stage), + datasets: [ + { + label: 'Deals Count', + data: pipelineHealth.map(p => p.count), + backgroundColor: 'rgba(113, 75, 103, 0.8)', // odoo-primary + borderColor: 'rgba(113, 75, 103, 1)', + borderWidth: 1, + borderRadius: 4, + yAxisID: 'y', + }, + { + label: 'Value (₹ Lakhs)', + data: pipelineHealth.map(p => p.value / 100000), + backgroundColor: 'rgba(0, 160, 157, 0.85)', // odoo-secondary + borderColor: 'rgba(0, 160, 157, 1)', + borderWidth: 1, + borderRadius: 4, + yAxisID: 'y1', + } + ] + }; + + const pipelineChartOptions = { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: 'top' as const, + labels: { font: { family: 'Inter', size: 12 } } + }, + tooltip: { + callbacks: { + label: function(context: any) { + if (context.datasetIndex === 0) { + return `Deals: ${context.raw}`; + } else { + return `Value: ₹${context.raw.toFixed(2)}L`; + } + } + } + } + }, + scales: { + y: { + type: 'linear' as const, + position: 'left' as const, + title: { display: true, text: 'Deals Count', font: { family: 'Inter', weight: 'bold' } }, + grid: { color: 'rgba(0, 0, 0, 0.05)' }, + ticks: { font: { family: 'Inter', size: 10 } } + }, + y1: { + type: 'linear' as const, + position: 'right' as const, + title: { display: true, text: 'Value (₹ Lakhs)', font: { family: 'Inter', weight: 'bold' } }, + grid: { drawOnChartArea: false }, // only draw grid lines for first y-axis + ticks: { font: { family: 'Inter', size: 10 } } + }, + x: { + grid: { display: false }, + ticks: { font: { family: 'Inter', size: 11, weight: 'bold' } } + } + } + }; + + // Get current performer selection + const activePerformers = performers[performerPeriod]; + + return ( +
+ {/* Header */} +
+
+

+ + Manager Command Center +

+

+ + Team intelligence, pipeline diagnostics & sales governance +

+
+
+ + + {teamPerformance.length} Active Staff Tracked + +
+
+ + {/* Target vs Sales Conversion Ratio - 3 column grid */} +
+ {/* This Month */} +
+
+

This Month

+ + Current Period + +
+
+
+ Achieved Ratio + {conversionRatio.thisMonth.ratio}% +
+
+
+
+
+
+ Target + {formatLakhs(conversionRatio.thisMonth.target)} +
+
+ Revenue + {formatLakhs(conversionRatio.thisMonth.revenue)} +
+
+
+ {conversionRatio.thisMonth.closures} closures + Enq. Conv: {conversionRatio.thisMonth.conversionRate}% +
+
+
+ + {/* Last Month */} +
+
+

Last Month

+ + Completed + +
+
+
+ Achieved Ratio + {conversionRatio.lastMonth.ratio}% +
+
+
+
+
+
+ Target + {formatLakhs(conversionRatio.lastMonth.target)} +
+
+ Revenue + {formatLakhs(conversionRatio.lastMonth.revenue)} +
+
+
+ {conversionRatio.lastMonth.closures} closures + Enq. Conv: {conversionRatio.lastMonth.conversionRate}% +
+
+
+ + {/* Quarter */} +
+
+

Quarterly Cumulative

+ + QTR + +
+
+
+ Achieved Ratio + {conversionRatio.quarter.ratio}% +
+
+
+
+
+
+ Target + {formatLakhs(conversionRatio.quarter.target)} +
+
+ Revenue + {formatLakhs(conversionRatio.quarter.revenue)} +
+
+
+ {conversionRatio.quarter.closures} closures + Enq. Conv: {conversionRatio.quarter.conversionRate}% +
+
+
+
+ + {/* Performers vs Underperformers section */} +
+
+
+

+ + Performers Analytics +

+

Top performing vs underperforming resources based on scorecards

+
+ + {/* Toggle Period */} +
+ + +
+
+ +
+ {/* Top Performers (Green) */} +
+

+ + Top Performers (Top 3) +

+
+ {activePerformers.top.length > 0 ? activePerformers.top.map((item, index) => ( +
+
+ + {index + 1} + +
+ {item.name} + {item.role.replace('_', ' ')} +
+
+ {item.score}/100 +
+ )) : ( +

No performance score registered in this window

+ )} +
+
+ + {/* Underperformers (Red) */} +
+

+ + Requires Improvement (Bottom 3) +

+
+ {activePerformers.under.length > 0 ? activePerformers.under.map((item, index) => ( +
+
+ + {activePerformers.under.length - index} + +
+ {item.name} + {item.role.replace('_', ' ')} +
+
+ {item.score}/100 +
+ )) : ( +

No performance score registered in this window

+ )} +
+
+
+
+ + {/* Pipeline Health (Stage wise Chart) */} +
+
+

Pipeline Health Diagnosis

+

Aggregated sales funnel breakdown across all teams

+
+
+ +
+
+ + {/* Team Performance Master Grid */} +
+ {/* Team Performance Table */} +
+
+

Team Scores & Performance Index

+

Latest overall scores and functional scorecard index

+
+ +
+ + + + + + + + + + + + {teamPerformance.map(member => { + let tagBg = 'bg-gray-100 text-gray-700'; + if (member.tag === 'EXCELLENT') tagBg = 'bg-emerald-100 text-emerald-800 border border-emerald-200'; + else if (member.tag === 'GOOD') tagBg = 'bg-blue-100 text-blue-800 border border-blue-200'; + else if (member.tag === 'AVERAGE') tagBg = 'bg-amber-100 text-amber-800 border border-amber-200'; + else if (member.tag === 'NEEDS_IMPROVEMENT') tagBg = 'bg-rose-100 text-rose-800 border border-rose-200'; + + return ( + + + + + + + + ); + })} + +
Staff MemberSales ScoreActivity ScoreOverall IndexGovernance Status
+ {member.name} + {member.role.replace('_', ' ')} + + {member.revenueScore}/100 + + {member.activityScore}/100 + +
+ {member.score} +
+
75 ? 'bg-emerald-500' : member.score > 50 ? 'bg-amber-500' : 'bg-rose-500'}`} + style={{ width: `${member.score}%` }} + /> +
+
+
+ + {member.tag.replace('_', ' ')} + +
+
+
+ + {/* Performance Weightage Matrix (70% Sales + 30% Activity) */} +
+
+

Performance Weightage Matrix

+

Calculated using 70% Sales performance weight + 30% Activity metrics weight

+
+ +
+ + + + + + + + + + + {performanceWeightage.map(pw => ( + + + + + + + ))} + +
StaffSales (70)Act. (30)Weighted
{pw.name}{pw.weightedSales}{pw.weightedActivity}{pw.totalWeighted}
+
+
+
+ + {/* Activities Analysis & Lead split */} +
+ {/* Activities scheduled vs completed */} +
+
+

Activities Completion Index

+

Detailed scheduled vs completed tasks by key sales activity categories

+
+ +
+ + + + + + + + + + + {activitiesAnalysis.map(act => { + let progressColor = 'bg-gray-400'; + if (act.completionRate >= 80) progressColor = 'bg-emerald-500'; + else if (act.completionRate >= 50) progressColor = 'bg-amber-500'; + else progressColor = 'bg-rose-500'; + + return ( + + + + + + + ); + })} + +
Activity CategoryScheduledCompletedCompletion Rate
+ {act.type.replace('_', ' ')} + + {act.scheduled} + + {act.completed} + +
+ {act.completionRate}% +
+
+
+
+
+
+
+ + {/* Lead vs Shared Conversion Analysis */} +
+
+

Collaboration vs Direct Funnel

+

Comparing conversions of single-owner accounts against shared collaborator contracts

+
+ +
+ {/* Own Business */} +
+ Own Business +
+ {formatLakhs(leadVsShared.own.value)} + {leadVsShared.own.count} closures +
+
+ + {leadVsShared.own.conversionRate}% Rate +
+
+ + {/* Shared Business */} +
+ Shared Business +
+ {formatLakhs(leadVsShared.shared.value)} + {leadVsShared.shared.count} closures +
+
+ + {leadVsShared.shared.conversionRate}% Rate +
+
+
+ +
+ Total enquiries analyzed in this period: {leadVsShared.totalEnquiries} +
+
+
+ + {/* Revenue Contribution Tracker */} +
+

Collaborative Revenue Ledger

+

Detailed contract transaction list with 50/50 revenue attribution split metrics

+ +
+ + + + + + + + + + + + {revenueContribution.length > 0 ? revenueContribution.map((rc, idx) => ( + + + + + + + + )) : ( + + + + )} + +
Lead Origin / CreatorClosing ResourceFull ValueStructureAttributed Shares (50/50 split)
+ {rc.leadOwner} + + {rc.closingOwner} + + {formatCurrency(rc.value)} + + {rc.isOwn ? ( + + SOLE + + ) : ( + + SHARED SPLIT + + )} + + {rc.isOwn ? ( + {formatCurrency(rc.value)} full + ) : ( + + Creator: {formatCurrency(rc.leadOwnerShare)} | Closer: {formatCurrency(rc.closingOwnerShare)} + + )} +
+ No won sales transactions reported this month. +
+
+
+
+ ); +} diff --git a/src/components/MyDashboard.tsx b/src/components/MyDashboard.tsx new file mode 100644 index 0000000..c40234d --- /dev/null +++ b/src/components/MyDashboard.tsx @@ -0,0 +1,560 @@ +'use client'; + +import React, { useEffect, useState } from 'react'; +import { + IndianRupee, + TrendingUp, + CheckCircle, + Percent, + ArrowUpRight, + AlertCircle, + Briefcase, + Award, + Users, + ChevronRight, + Calendar, + Target +} from 'lucide-react'; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + BarElement, + Title, + Tooltip, + Legend, + ArcElement +} from 'chart.js'; +import { Bar, Doughnut } from 'react-chartjs-2'; +import api from '../lib/axios'; + +ChartJS.register( + CategoryScale, + LinearScale, + BarElement, + Title, + Tooltip, + Legend, + ArcElement +); + +interface TargetData { + monthly: number; + minimum: number; + weekly: number; + dailyLead: number; + requiredClosures: number | null; + requiredDemos: number | null; +} + +interface WeeklyItem { + label: string; + expected: number; + actual: number; +} + +interface StageItem { + stage: string; + count: number; + value: number; +} + +interface SalesBreakdown { + value: number; + count: number; + pct: number; +} + +interface MyDashboardData { + target: TargetData | null; + weekly: WeeklyItem[]; + totalExpected: number; + totalActual: number; + achievementPct: number; + remaining: number; + remainingPct: number; + stageData: StageItem[]; + ownSales: SalesBreakdown; + sharedSales: SalesBreakdown; +} + +export default function MyDashboard() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const currentMonthName = new Date().toLocaleString('default', { month: 'long' }); + const currentYear = new Date().getFullYear(); + + useEffect(() => { + const fetchMyDashboard = async () => { + try { + setLoading(true); + const res = await api.get('/dashboard/my'); + setData(res.data); + } catch (err: any) { + console.error("Failed to load My Dashboard data", err); + setError(err.response?.data?.message || "Could not retrieve dashboard data. Please try again later."); + } finally { + setLoading(false); + } + }; + fetchMyDashboard(); + }, []); + + const formatLakhs = (value: number) => { + return `₹${(value / 100000).toFixed(2)}L`; + }; + + const formatCurrency = (value: number) => { + return new Intl.NumberFormat('en-IN', { + style: 'currency', + currency: 'INR', + maximumFractionDigits: 0 + }).format(value); + }; + + if (loading) { + return ( +
+
+

Preparing your sales dashboard...

+
+ ); + } + + if (error || !data) { + return ( +
+ +

Error Loading Dashboard

+

{error || "No data received from server."}

+ +
+ ); + } + + const { + target, + weekly, + totalExpected, + totalActual, + achievementPct, + remaining, + remainingPct, + stageData, + ownSales, + sharedSales + } = data; + + // Weekly Chart Data + const weeklyChartData = { + labels: weekly.map(w => w.label), + datasets: [ + { + label: 'Expected Target', + data: weekly.map(w => w.expected), + backgroundColor: 'rgba(113, 75, 103, 0.4)', // Soft odoo-primary + borderColor: 'rgba(113, 75, 103, 0.8)', + borderWidth: 1.5, + borderRadius: 6, + barPercentage: 0.6, + categoryPercentage: 0.8, + }, + { + label: 'Actual Achieved', + data: weekly.map(w => w.actual), + backgroundColor: 'rgba(0, 160, 157, 0.85)', // Strong odoo-secondary + borderColor: 'rgba(0, 160, 157, 1)', + borderWidth: 1.5, + borderRadius: 6, + barPercentage: 0.6, + categoryPercentage: 0.8, + } + ] + }; + + const weeklyChartOptions = { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: 'top' as const, + labels: { + boxWidth: 12, + font: { family: 'Inter', size: 12 } + } + }, + tooltip: { + callbacks: { + label: function(context: any) { + return `${context.dataset.label}: ${formatCurrency(context.raw)}`; + } + } + } + }, + scales: { + y: { + grid: { color: 'rgba(0, 0, 0, 0.05)' }, + ticks: { + callback: function(value: any) { + return formatLakhs(value); + }, + font: { family: 'Inter', size: 10 } + } + }, + x: { + grid: { display: false }, + ticks: { font: { family: 'Inter', size: 11 } } + } + } + }; + + // Own vs Shared Doughnut Data + const salesTypeData = { + labels: ['Own Sales', 'Shared Sales'], + datasets: [ + { + data: [ownSales.value, sharedSales.value], + backgroundColor: [ + 'rgba(113, 75, 103, 0.9)', // odoo-primary + 'rgba(0, 160, 157, 0.9)', // odoo-secondary + ], + hoverBackgroundColor: [ + 'rgba(113, 75, 103, 1)', + 'rgba(0, 160, 157, 1)', + ], + borderWidth: 2, + borderColor: '#ffffff', + } + ] + }; + + const salesTypeOptions = { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { display: false }, + tooltip: { + callbacks: { + label: function(context: any) { + const total = ownSales.value + sharedSales.value; + const val = context.raw; + const pct = total > 0 ? ((val / total) * 100).toFixed(1) : 0; + return ` ${context.label}: ${formatCurrency(val)} (${pct}%)`; + } + } + } + }, + cutout: '70%' + }; + + // Calculate progression percent for visual progress bar + const progressPercent = totalExpected > 0 ? Math.min(100, (totalActual / totalExpected) * 100) : 0; + + return ( +
+ {/* Header section with greeting */} +
+
+

My Sales Dashboard

+

+ + Performance tracking for {currentMonthName} {currentYear} +

+
+ {target && ( +
+
+ Monthly Target + {formatCurrency(target.monthly)} +
+
+
+ MTD Achieved + {formatCurrency(totalActual)} +
+
+ )} +
+ + {/* KPI Cards Grid */} +
+ {/* Card 1: Achievement % */} +
+
+
+

Achievement

+

{achievementPct}%

+
+
+ +
+
+
+
+
+
+
+
+ + {/* Card 2: Remaining Target */} +
+
+
+

Remaining Target

+

{formatCurrency(remaining)}

+
+
+ +
+
+

+ {remainingPct}% of target left to achieve +

+
+ + {/* Card 3: Own Sales % */} +
+
+
+

Own Sales Contribution

+

{ownSales.pct}%

+
+
+ +
+
+

+ Value: {formatLakhs(ownSales.value)} ({ownSales.count} deals) +

+
+ + {/* Card 4: Shared Sales % */} +
+
+
+

Shared Sales Contribution

+

{sharedSales.pct}%

+
+
+ +
+
+

+ Value: {formatLakhs(sharedSales.value)} ({sharedSales.count} deals) +

+
+
+ + {/* Target Progress Bar Detail */} +
+
+
+

+ + Target vs Achieved Progression +

+

Real-time status of your targets for the month

+
+ + + {progressPercent.toFixed(1)}% Completed + +
+ +
+
+ {progressPercent > 8 && ( + + {progressPercent.toFixed(0)}% + + )} +
+
+ +
+ MTD Achieved: {formatCurrency(totalActual)} + Monthly Target: {formatCurrency(totalExpected)} +
+
+ + {/* Charts Section: Weekly Bar + Own/Shared Pie */} +
+ {/* Grouped Weekly expected vs actual */} +
+
+

Week-wise Expected vs Actual Value

+

Comparison of weekly targets against actual revenue contributions

+
+
+ +
+
+ + {/* Own vs Shared Doughnut */} +
+
+

Sales Attribution

+

Split between sole closure vs collaborative sales split

+
+
+ +
+ + {formatLakhs(ownSales.value + sharedSales.value)} + + Total Sales +
+
+ +
+
+
+
+ Own Sales +
+
+ {formatCurrency(ownSales.value)} + {ownSales.count} Deals • {ownSales.pct}% +
+
+
+
+
+ Shared Sales +
+
+ {formatCurrency(sharedSales.value)} + {sharedSales.count} Deals • {sharedSales.pct}% +
+
+
+
+
+ + {/* Stage-wise Pipeline & Target Breakdown Grid */} +
+ {/* Stage-wise pipeline count & value */} +
+

Stage-wise Pipeline Count & Value

+

Breakdown of opportunities in active stages of the funnel

+ +
+ {stageData.map((item, index) => { + const maxValue = Math.max(...stageData.map(s => s.value), 1); + const percent = (item.value / maxValue) * 100; + + // Color coding according to stage + let barColor = 'bg-gray-400'; + let textColor = 'text-gray-600'; + if (item.stage === 'LEAD') { barColor = 'bg-[#3b82f6]'; textColor = 'text-[#2563eb]'; } + else if (item.stage === 'QUALIFIED') { barColor = 'bg-[#8b5cf6]'; textColor = 'text-[#7c3aed]'; } + else if (item.stage === 'POTENTIAL') { barColor = 'bg-[#f59e0b]'; textColor = 'text-[#d97706]'; } + else if (item.stage === 'DEMO') { barColor = 'bg-[#ec4899]'; textColor = 'text-[#db2777]'; } + else if (item.stage === 'SALES') { barColor = 'bg-[#10b981]'; textColor = 'text-[#059669]'; } + + return ( +
+
+
+ {item.stage} +
+ +
+
+
+
+ +
+ {item.count} deals + {formatLakhs(item.value)} +
+
+
+ ); + })} +
+
+ + {/* Target Summary Widget */} +
+
+

Target Summary

+

Metrics required to meet goals this month

+
+ + {target ? ( +
+
+
+ Monthly Target + {formatCurrency(target.monthly)} +
+ 100% +
+ +
+
+ Minimum Target + {formatCurrency(target.minimum)} +
+ Min Threshold +
+ +
+
+ Weekly Target + {formatCurrency(target.weekly)} +
+ 4 Weeks split +
+ +
+
+ Daily Lead Target + {target.dailyLead} Leads / Day +
+ Volume metric +
+ + {(target.requiredClosures || target.requiredDemos) && ( +
+ {target.requiredClosures && ( +
+ Closures Needed + {target.requiredClosures} +
+ )} + {target.requiredDemos && ( +
+ Demos Needed + {target.requiredDemos} +
+ )} +
+ )} +
+ ) : ( +
+ + No target defined + Ask your manager to set your monthly goals for {currentMonthName} +
+ )} +
+
+
+ ); +} diff --git a/src/components/OpportunityBoard.tsx b/src/components/OpportunityBoard.tsx index a9d69d1..a3b3917 100644 --- a/src/components/OpportunityBoard.tsx +++ b/src/components/OpportunityBoard.tsx @@ -1,7 +1,7 @@ "use client"; import React, { useState, useEffect } from 'react'; -import { X, Pencil, Plus, TrendingUp, Star, MoreHorizontal, Clock, CheckCircle2, AlertCircle, Paperclip, FileText, Download } from 'lucide-react'; +import { X, Pencil, Plus, TrendingUp, Star, MoreHorizontal, Clock, CheckCircle2, AlertCircle, Paperclip, FileText, Download, Search } from 'lucide-react'; import { DndContext, closestCenter, @@ -36,6 +36,7 @@ interface Opportunity { client: { name: string; id: string; + email?: string; companyName?: string; contactName?: string; closingProbability?: number; @@ -280,6 +281,20 @@ export default function OpportunityBoard() { const [assignees, setAssignees] = useState([]); const [products, setProducts] = useState([]); const [loadingAssignees, setLoadingAssignees] = useState(false); + const [clientSearchTerm, setClientSearchTerm] = useState(''); + + const filteredItems = items.filter(item => { + if (!clientSearchTerm) return true; + const term = clientSearchTerm.toLowerCase(); + const clientName = item.client?.name?.toLowerCase() || ''; + const companyName = item.client?.companyName?.toLowerCase() || ''; + const contactName = item.client?.contactName?.toLowerCase() || ''; + const clientEmail = item.client?.email?.toLowerCase() || ''; + return clientName.includes(term) || + companyName.includes(term) || + contactName.includes(term) || + clientEmail.includes(term); + }); const getFileUrl = (url: string) => { if (!url) return '#'; @@ -570,7 +585,7 @@ export default function OpportunityBoard() { }; const stages = ['LEAD', 'QUALIFIED', 'POTENTIAL', 'SALES']; - const getColumnTotal = (stage: string) => items.filter(i => i.stage === stage).reduce((sum, i) => sum + i.value, 0); + const getColumnTotal = (stage: string) => filteredItems.filter(i => i.stage === stage).reduce((sum, i) => sum + i.value, 0); return (
@@ -590,12 +605,31 @@ export default function OpportunityBoard() {
-
+
+
+ + setClientSearchTerm(e.target.value)} + /> + {clientSearchTerm && ( + + )} +
+
Total Pipeline: - ₹{items.reduce((sum, i) => sum + i.value, 0).toLocaleString()} + ₹{filteredItems.reduce((sum, i) => sum + i.value, 0).toLocaleString()}
@@ -614,7 +648,7 @@ export default function OpportunityBoard() { key={stage} id={stage} title={STAGE_CONFIG[stage].title} - items={items.filter((i) => i.stage === stage)} + items={filteredItems.filter((i) => i.stage === stage)} totalValue={getColumnTotal(stage)} onAddClick={handleCreateClick} onEditClick={handleEditClick}