parent
9a875aea9f
commit
107126b1f4
|
|
@ -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<DashboardStats | null>(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 (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-odoo-primary"></div>
|
||||
|
|
@ -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 (
|
||||
<div className="p-1 space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700">
|
||||
{/* Header / Welcome */}
|
||||
<div className="flex flex-col md:flex-row md:items-end justify-between px-2">
|
||||
<div>
|
||||
<h1 className="text-3xl font-black text-slate-800 tracking-tight">
|
||||
Dashboard
|
||||
</h1>
|
||||
<p className="text-slate-500 font-medium">
|
||||
Welcome back, <span className="text-odoo-primary font-bold">{user?.name}</span>. Here's what's happening today.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-4 md:mt-0 flex items-center space-x-2">
|
||||
<button className="flex items-center space-x-2 bg-white border border-slate-200 px-4 py-2 rounded-xl text-sm font-bold text-slate-600 hover:bg-slate-50 transition-all shadow-sm">
|
||||
<Filter size={16} />
|
||||
<span>Filter</span>
|
||||
</button>
|
||||
<button className="bg-odoo-primary text-white p-2.5 rounded-xl shadow-lg shadow-odoo-primary/20 hover:scale-105 transition-all">
|
||||
<Search size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* KPI Cards */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{kpiCards.map((card, i) => (
|
||||
<div key={i} className="odoo-card p-6 flex flex-col relative overflow-hidden group hover:shadow-xl transition-all duration-300">
|
||||
<div className={`absolute top-0 right-0 w-24 h-24 ${card.color} opacity-[0.03] rounded-bl-full translate-x-4 -translate-y-4 group-hover:scale-110 transition-transform`} />
|
||||
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className={`p-3 rounded-2xl ${card.color} bg-opacity-10 text-white`}>
|
||||
<card.icon size={24} className={card.color.replace('bg-', 'text-')} />
|
||||
</div>
|
||||
<div className={`flex items-center text-[10px] font-black px-2 py-1 rounded-full ${card.trendUp ? 'bg-emerald-50 text-emerald-600' : 'bg-rose-50 text-rose-600'}`}>
|
||||
{card.trendUp ? <ArrowUpRight size={12} className="mr-0.5" /> : <ArrowDownRight size={12} className="mr-0.5" />}
|
||||
{card.trend}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-2xl font-black text-slate-800 tracking-tight">{card.value}</h3>
|
||||
<p className="text-xs font-bold text-slate-400 uppercase tracking-widest">{card.label}</p>
|
||||
{card.subValue && <span className="text-[10px] text-slate-300 font-medium">{card.subValue}</span>}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Main Stats Area */}
|
||||
<div className="lg:col-span-2 space-y-8">
|
||||
{/* Performance Warnings (Quarterly Logic) - ADMIN ONLY */}
|
||||
{(user?.role === 'ADMIN' || user?.role === 'GENERAL_MANAGER') && quarterly && quarterly.status !== 'NORMAL' && (
|
||||
<div className={`rounded-[24px] p-6 flex items-center justify-between group shadow-lg animate-pulse ${
|
||||
quarterly.status === 'ACTION' ? 'bg-rose-600 text-white' : 'bg-amber-500 text-white'
|
||||
}`}>
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="bg-white/20 p-3 rounded-2xl">
|
||||
<AlertCircle size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-black text-lg uppercase tracking-tight">
|
||||
Performance {quarterly.status}
|
||||
</h4>
|
||||
<p className="text-sm font-bold opacity-80">
|
||||
{quarterly.status === 'ACTION'
|
||||
? "Immediate improvement required to meet organizational standards."
|
||||
: "Performance has been below minimum for 2 months. Attention required."
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white/20 px-4 py-2 rounded-xl font-black text-xs uppercase">
|
||||
{quarterly.status}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Team Performance Leaderboard - ADMIN ONLY */}
|
||||
{(user?.role === 'ADMIN' || user?.role === 'GENERAL_MANAGER') && (
|
||||
<TeamPerformance />
|
||||
)}
|
||||
|
||||
{/* Efficiency Funnel */}
|
||||
<FunnelAnalytics />
|
||||
|
||||
{/* Improvement Suggestions - ADMIN ONLY */}
|
||||
{(user?.role === 'ADMIN' || user?.role === 'GENERAL_MANAGER') && quarterly && quarterly.suggestions.length > 0 && (
|
||||
<div className="odoo-card p-6 border-l-4 border-indigo-500 bg-indigo-50/30">
|
||||
<h3 className="font-bold text-slate-800 mb-4 flex items-center">
|
||||
<TrendingUp className="mr-2 text-indigo-500" size={18} />
|
||||
Performance Improvement Plan
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{quarterly.suggestions.map((suggestion, idx) => (
|
||||
<div key={idx} className="flex items-start space-x-3">
|
||||
<div className="mt-1 w-1.5 h-1.5 rounded-full bg-indigo-400 shrink-0" />
|
||||
<p className="text-sm text-slate-600 font-medium leading-relaxed">{suggestion}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Pending Actions (Conditional) */}
|
||||
{(user?.role === 'ADMIN' || user?.role === 'MANAGER') && kpis.pendingExpenses > 0 && (
|
||||
<div className="bg-rose-50 border border-rose-100 rounded-[24px] p-6 flex items-center justify-between group cursor-pointer hover:bg-rose-100/50 transition-all">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="bg-rose-500 text-white p-3 rounded-2xl shadow-lg shadow-rose-200">
|
||||
<IndianRupee size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-extrabold text-rose-900">Attention Required</h4>
|
||||
<p className="text-sm text-rose-700/70 font-medium">You have {kpis.pendingExpenses} pending expense claims to approve.</p>
|
||||
</div>
|
||||
</div>
|
||||
<ArrowRight className="text-rose-400 group-hover:translate-x-1 transition-transform" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Recent Enquiries List */}
|
||||
<div className="odoo-card overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-slate-50 flex items-center justify-between">
|
||||
<h3 className="font-bold text-slate-800">Latest Enquiries</h3>
|
||||
<button className="text-[11px] font-black text-odoo-primary uppercase tracking-widest hover:underline">View All</button>
|
||||
</div>
|
||||
<div className="divide-y divide-slate-50">
|
||||
{recentActivity.enquiries.map((enq, index) => (
|
||||
<div key={index} className="px-6 py-4 flex items-center justify-between hover:bg-slate-50/50 transition-all group">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="w-10 h-10 rounded-xl bg-indigo-50 flex items-center justify-center text-indigo-500 font-black group-hover:bg-indigo-500 group-hover:text-white transition-all">
|
||||
{enq.client?.name?.charAt(0) || 'E'}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-bold text-slate-800">{enq.client?.name || 'Quick Enquiry'}</h4>
|
||||
<p className="text-xs text-slate-400">{enq.user?.name} • {formatDistanceToNow(new Date(enq.createdAt), { addSuffix: true })}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="hidden sm:block text-right">
|
||||
<p className="text-xs font-black text-slate-700">New Lead</p>
|
||||
<p className="text-[10px] text-slate-300 font-bold uppercase tracking-tighter">Inbound</p>
|
||||
</div>
|
||||
<div className="p-2 rounded-lg bg-slate-50 text-slate-400 opacity-0 group-hover:opacity-100 transition-all">
|
||||
<ArrowRight size={14} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sidebar Activity */}
|
||||
<div className="space-y-8">
|
||||
{/* Target Achievement Widget */}
|
||||
<div className="odoo-card p-6">
|
||||
<h3 className="font-bold text-slate-800 mb-6">Target Achievement</h3>
|
||||
{target ? (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<div className="flex justify-between text-xs font-bold mb-2">
|
||||
<span className="text-slate-400">MONTHLY TARGET</span>
|
||||
<span className="text-odoo-primary">₹{(target.monthly / 1000).toFixed(0)}k</span>
|
||||
</div>
|
||||
<div className="h-3 bg-slate-100 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-odoo-primary transition-all duration-1000"
|
||||
style={{ width: `${Math.min(100, (target.achieved / target.monthly) * 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-[10px] text-slate-400 mt-2 font-medium">Achieved: ₹{(target.achieved / 1000).toFixed(1)}k</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex justify-between text-xs font-bold mb-2">
|
||||
<span className="text-slate-400">MINIMUM TARGET</span>
|
||||
<span className="text-amber-600">₹{(target.minimum / 1000).toFixed(0)}k</span>
|
||||
</div>
|
||||
<div className="h-3 bg-slate-100 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-amber-500 transition-all duration-1000"
|
||||
style={{ width: `${Math.min(100, (target.achieved / target.minimum) * 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
{target.achieved >= target.minimum ? (
|
||||
<p className="text-[10px] text-emerald-600 mt-2 font-bold flex items-center">
|
||||
<CheckCircle2 size={10} className="mr-1" /> MINIMUM REACHED
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-[10px] text-amber-600 mt-2 font-bold">₹{((target.minimum - target.achieved)/1000).toFixed(1)}k to go</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Benchmarks */}
|
||||
<div className="grid grid-cols-2 gap-4 pt-4 border-t border-slate-50">
|
||||
<div className="bg-slate-50 p-3 rounded-xl">
|
||||
<p className="text-[9px] font-black text-slate-400 uppercase tracking-tighter mb-1">Daily Leads</p>
|
||||
<p className="text-sm font-black text-slate-700">{target.dailyLead}</p>
|
||||
</div>
|
||||
<div className="bg-slate-50 p-3 rounded-xl">
|
||||
<p className="text-[9px] font-black text-slate-400 uppercase tracking-tighter mb-1">Req. Demos</p>
|
||||
<p className="text-sm font-black text-slate-700">{target.requiredDemos || Math.ceil(target.monthly / 40000) * 3}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-8 text-center">
|
||||
<AlertCircle className="text-slate-200 mb-2" size={32} />
|
||||
<p className="text-xs text-slate-400 font-bold">NO TARGET SET</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Performance Breakdown */}
|
||||
<div className="odoo-card p-6">
|
||||
<h3 className="font-bold text-slate-800 mb-6">Performance Mix</h3>
|
||||
{performance ? (
|
||||
<div className="space-y-4">
|
||||
{[
|
||||
{ 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) => (
|
||||
<div key={idx}>
|
||||
<div className="flex justify-between text-[10px] font-black uppercase tracking-widest mb-1.5">
|
||||
<span className="text-slate-400">{item.label}</span>
|
||||
<span className="text-slate-800">{Math.round(item.score)}/{item.max}</span>
|
||||
</div>
|
||||
<div className="h-1.5 bg-slate-50 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full ${item.color} transition-all duration-700`}
|
||||
style={{ width: `${(item.score / item.max) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-slate-400 text-center py-4">No data available</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Marketing Impact Breakdown */}
|
||||
<div className="odoo-card p-6">
|
||||
<h3 className="font-bold text-slate-800 mb-6 flex items-center">
|
||||
<TrendingUp className="mr-2 text-indigo-500" size={18} />
|
||||
Marketing Impact
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{ 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) => (
|
||||
<div key={i} className={`flex items-center justify-between p-3 rounded-xl ${act.bg}`}>
|
||||
<span className={`text-[10px] font-black uppercase tracking-widest ${act.color}`}>{act.label}</span>
|
||||
<span className="text-lg font-black text-slate-800">{act.count}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-[10px] text-slate-400 mt-4 text-center font-bold italic underline cursor-pointer hover:text-odoo-primary transition-colors">View Detailed Reports</p>
|
||||
</div>
|
||||
|
||||
{/* Recent Opportunities Updates */}
|
||||
<div className="odoo-card overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-slate-50">
|
||||
<h3 className="font-bold text-slate-800">Pipeline Pulse</h3>
|
||||
</div>
|
||||
<div className="p-6 space-y-6">
|
||||
{recentActivity.opportunities.map((op, index) => (
|
||||
<div key={index} className="flex space-x-4 relative">
|
||||
{index !== recentActivity.opportunities.length - 1 && (
|
||||
<div className="absolute left-[7px] top-8 bottom-[-24px] w-[2px] bg-slate-100" />
|
||||
)}
|
||||
<div className={`mt-1.5 w-4 h-4 rounded-full border-4 border-white shadow-sm shrink-0 z-10 ${
|
||||
op.stage === 'SALES' ? 'bg-emerald-500' : 'bg-odoo-primary'
|
||||
}`} />
|
||||
<div>
|
||||
<p className="text-xs font-black text-slate-800 leading-tight">
|
||||
{op.title}
|
||||
</p>
|
||||
<p className="text-[10px] text-slate-400 font-medium mt-0.5">
|
||||
Updated to <span className="text-odoo-primary font-bold">{op.stage}</span> • {formatDistanceToNow(new Date(op.updatedAt))} ago
|
||||
</p>
|
||||
<div className="mt-2 text-[13px] font-bold text-slate-700">
|
||||
₹{op.value.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* System Guard Extra Widget */}
|
||||
<div className="bg-odoo-primary rounded-[28px] p-6 text-white relative overflow-hidden">
|
||||
<div className="absolute top-[-20px] right-[-20px] w-32 h-32 bg-white/10 rounded-full blur-2xl" />
|
||||
<h4 className="text-lg font-black mb-1">Growth Index</h4>
|
||||
<p className="text-xs text-white/60 mb-6 font-medium">Your productivity is up by 14% this week. Keep tracking your leads!</p>
|
||||
<div className="flex items-center space-x-3 text-xs font-black bg-white/10 w-fit px-3 py-1.5 rounded-full">
|
||||
<TrendingUp size={14} />
|
||||
<span>Level Up</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return isManager ? <ManagerDashboard /> : <MyDashboard />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, number>;
|
||||
performanceWeightage: PerformanceWeightageItem[];
|
||||
performers: PerformersGroup;
|
||||
leadVsShared: LeadVsSharedGroup;
|
||||
revenueContribution: RevenueContributionItem[];
|
||||
}
|
||||
|
||||
export default function ManagerDashboard() {
|
||||
const { user } = useAuth();
|
||||
const [data, setData] = useState<ManagerDashboardData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(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 (
|
||||
<div className="flex flex-col items-center justify-center h-96">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-odoo-secondary mb-4"></div>
|
||||
<p className="text-gray-500 font-medium">Assembling team performance statistics...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !data) {
|
||||
return (
|
||||
<div className="odoo-card p-8 text-center max-w-lg mx-auto my-12">
|
||||
<AlertCircle className="w-12 h-12 text-rose-500 mx-auto mb-4" />
|
||||
<h3 className="text-xl font-bold text-gray-800 mb-2">Access Denied / Error</h3>
|
||||
<p className="text-gray-600 mb-6">{error || "You might not have administrative privileges."}</p>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="px-6 py-2 bg-odoo-secondary text-white rounded-md font-semibold hover:bg-[#008f8c] transition-colors"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="space-y-8 pb-12 animate-fade-in">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 bg-gradient-to-r from-slate-800 via-odoo-primary to-odoo-secondary text-white p-6 rounded-2xl shadow-lg">
|
||||
<div>
|
||||
<h1 className="text-2xl md:text-3xl font-bold tracking-tight flex items-center gap-2">
|
||||
<Sparkles className="w-7 h-7 text-amber-300" />
|
||||
Manager Command Center
|
||||
</h1>
|
||||
<p className="text-white/80 text-sm mt-1 flex items-center gap-1.5">
|
||||
<Calendar className="w-4 h-4" />
|
||||
Team intelligence, pipeline diagnostics & sales governance
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white/10 backdrop-blur-md px-4 py-2.5 rounded-xl border border-white/10 flex items-center gap-2">
|
||||
<span className="w-2.5 h-2.5 bg-emerald-400 rounded-full animate-ping" />
|
||||
<span className="text-xs font-bold uppercase tracking-wider text-white/95">
|
||||
{teamPerformance.length} Active Staff Tracked
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Target vs Sales Conversion Ratio - 3 column grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{/* This Month */}
|
||||
<div className="odoo-card p-6 bg-gradient-to-b from-white to-purple-50/20 border-t-4 border-odoo-primary">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h4 className="text-sm font-bold text-odoo-primary uppercase tracking-widest">This Month</h4>
|
||||
<span className="text-xs font-black bg-purple-50 text-odoo-primary px-2.5 py-1 rounded-full border border-purple-100">
|
||||
Current Period
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between items-end">
|
||||
<span className="text-xs text-gray-500 font-bold uppercase">Achieved Ratio</span>
|
||||
<span className="text-3xl font-black text-gray-800">{conversionRatio.thisMonth.ratio}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-150 h-2.5 rounded-full overflow-hidden">
|
||||
<div className="bg-odoo-primary h-full rounded-full" style={{ width: `${Math.min(100, conversionRatio.thisMonth.ratio)}%` }} />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 pt-2 text-xs font-bold border-t border-gray-100">
|
||||
<div>
|
||||
<span className="text-[10px] text-gray-400 block uppercase font-medium">Target</span>
|
||||
<span className="text-gray-700 text-sm font-black">{formatLakhs(conversionRatio.thisMonth.target)}</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span className="text-[10px] text-gray-400 block uppercase font-medium">Revenue</span>
|
||||
<span className="text-emerald-600 text-sm font-black">{formatLakhs(conversionRatio.thisMonth.revenue)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between items-center text-xs font-semibold text-gray-500 pt-1">
|
||||
<span>{conversionRatio.thisMonth.closures} closures</span>
|
||||
<span>Enq. Conv: <strong className="text-gray-700">{conversionRatio.thisMonth.conversionRate}%</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Last Month */}
|
||||
<div className="odoo-card p-6 border-t-4 border-slate-400">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h4 className="text-sm font-bold text-gray-600 uppercase tracking-widest">Last Month</h4>
|
||||
<span className="text-xs font-black bg-gray-50 text-gray-500 px-2.5 py-1 rounded-full border border-gray-200">
|
||||
Completed
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between items-end">
|
||||
<span className="text-xs text-gray-500 font-bold uppercase">Achieved Ratio</span>
|
||||
<span className="text-3xl font-black text-gray-800">{conversionRatio.lastMonth.ratio}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-150 h-2.5 rounded-full overflow-hidden">
|
||||
<div className="bg-slate-400 h-full rounded-full" style={{ width: `${Math.min(100, conversionRatio.lastMonth.ratio)}%` }} />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 pt-2 text-xs font-bold border-t border-gray-100">
|
||||
<div>
|
||||
<span className="text-[10px] text-gray-400 block uppercase font-medium">Target</span>
|
||||
<span className="text-gray-700 text-sm font-black">{formatLakhs(conversionRatio.lastMonth.target)}</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span className="text-[10px] text-gray-400 block uppercase font-medium">Revenue</span>
|
||||
<span className="text-emerald-600 text-sm font-black">{formatLakhs(conversionRatio.lastMonth.revenue)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between items-center text-xs font-semibold text-gray-500 pt-1">
|
||||
<span>{conversionRatio.lastMonth.closures} closures</span>
|
||||
<span>Enq. Conv: <strong className="text-gray-700">{conversionRatio.lastMonth.conversionRate}%</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quarter */}
|
||||
<div className="odoo-card p-6 bg-gradient-to-b from-white to-teal-50/20 border-t-4 border-odoo-secondary">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h4 className="text-sm font-bold text-odoo-secondary uppercase tracking-widest">Quarterly Cumulative</h4>
|
||||
<span className="text-xs font-black bg-teal-50 text-odoo-secondary px-2.5 py-1 rounded-full border border-teal-100">
|
||||
QTR
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between items-end">
|
||||
<span className="text-xs text-gray-500 font-bold uppercase">Achieved Ratio</span>
|
||||
<span className="text-3xl font-black text-gray-800">{conversionRatio.quarter.ratio}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-150 h-2.5 rounded-full overflow-hidden">
|
||||
<div className="bg-odoo-secondary h-full rounded-full" style={{ width: `${Math.min(100, conversionRatio.quarter.ratio)}%` }} />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 pt-2 text-xs font-bold border-t border-gray-100">
|
||||
<div>
|
||||
<span className="text-[10px] text-gray-400 block uppercase font-medium">Target</span>
|
||||
<span className="text-gray-700 text-sm font-black">{formatLakhs(conversionRatio.quarter.target)}</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span className="text-[10px] text-gray-400 block uppercase font-medium">Revenue</span>
|
||||
<span className="text-emerald-600 text-sm font-black">{formatLakhs(conversionRatio.quarter.revenue)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between items-center text-xs font-semibold text-gray-500 pt-1">
|
||||
<span>{conversionRatio.quarter.closures} closures</span>
|
||||
<span>Enq. Conv: <strong className="text-gray-700">{conversionRatio.quarter.conversionRate}%</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Performers vs Underperformers section */}
|
||||
<div className="odoo-card p-6">
|
||||
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-6 gap-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-gray-800 flex items-center gap-2">
|
||||
<Award className="w-5 h-5 text-amber-500" />
|
||||
Performers Analytics
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500 mt-0.5">Top performing vs underperforming resources based on scorecards</p>
|
||||
</div>
|
||||
|
||||
{/* Toggle Period */}
|
||||
<div className="inline-flex p-1 bg-gray-100 rounded-xl border border-gray-200">
|
||||
<button
|
||||
onClick={() => setPerformerPeriod('month')}
|
||||
className={`px-4 py-1.5 text-xs font-bold rounded-lg transition-all ${performerPeriod === 'month' ? 'bg-white text-odoo-primary shadow-sm' : 'text-gray-500 hover:text-gray-700'}`}
|
||||
>
|
||||
Month Breakdown
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setPerformerPeriod('quarter')}
|
||||
className={`px-4 py-1.5 text-xs font-bold rounded-lg transition-all ${performerPeriod === 'quarter' ? 'bg-white text-odoo-primary shadow-sm' : 'text-gray-500 hover:text-gray-700'}`}
|
||||
>
|
||||
Quarter Cumulative
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
{/* Top Performers (Green) */}
|
||||
<div>
|
||||
<h4 className="text-sm font-bold text-emerald-600 mb-3 flex items-center gap-1">
|
||||
<ArrowUpRight className="w-4 h-4" />
|
||||
Top Performers (Top 3)
|
||||
</h4>
|
||||
<div className="space-y-3">
|
||||
{activePerformers.top.length > 0 ? activePerformers.top.map((item, index) => (
|
||||
<div key={item.id} className="flex justify-between items-center p-3.5 bg-emerald-50/50 hover:bg-emerald-50 rounded-xl border border-emerald-100 transition-all">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="w-6 h-6 flex items-center justify-center bg-emerald-500 text-white rounded-full text-xs font-black shadow-sm">
|
||||
{index + 1}
|
||||
</span>
|
||||
<div>
|
||||
<span className="font-extrabold text-sm text-gray-800 block">{item.name}</span>
|
||||
<span className="text-[10px] text-gray-500 uppercase tracking-wider font-semibold">{item.role.replace('_', ' ')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-base font-black text-emerald-700">{item.score}/100</span>
|
||||
</div>
|
||||
)) : (
|
||||
<p className="text-xs text-gray-400 italic p-4 text-center">No performance score registered in this window</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Underperformers (Red) */}
|
||||
<div>
|
||||
<h4 className="text-sm font-bold text-rose-500 mb-3 flex items-center gap-1">
|
||||
<TrendingDown className="w-4 h-4" />
|
||||
Requires Improvement (Bottom 3)
|
||||
</h4>
|
||||
<div className="space-y-3">
|
||||
{activePerformers.under.length > 0 ? activePerformers.under.map((item, index) => (
|
||||
<div key={item.id} className="flex justify-between items-center p-3.5 bg-rose-50/50 hover:bg-rose-50 rounded-xl border border-rose-100 transition-all">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="w-6 h-6 flex items-center justify-center bg-rose-500 text-white rounded-full text-xs font-black shadow-sm">
|
||||
{activePerformers.under.length - index}
|
||||
</span>
|
||||
<div>
|
||||
<span className="font-extrabold text-sm text-gray-800 block">{item.name}</span>
|
||||
<span className="text-[10px] text-gray-500 uppercase tracking-wider font-semibold">{item.role.replace('_', ' ')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-base font-black text-rose-700">{item.score}/100</span>
|
||||
</div>
|
||||
)) : (
|
||||
<p className="text-xs text-gray-400 italic p-4 text-center">No performance score registered in this window</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pipeline Health (Stage wise Chart) */}
|
||||
<div className="odoo-card p-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-gray-800">Pipeline Health Diagnosis</h3>
|
||||
<p className="text-xs text-gray-500 mb-6">Aggregated sales funnel breakdown across all teams</p>
|
||||
</div>
|
||||
<div className="h-80">
|
||||
<Bar options={pipelineChartOptions} data={pipelineChartData} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Team Performance Master Grid */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Team Performance Table */}
|
||||
<div className="odoo-card p-6 lg:col-span-2 overflow-hidden flex flex-col justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-gray-800 mb-1">Team Scores & Performance Index</h3>
|
||||
<p className="text-xs text-gray-500 mb-4">Latest overall scores and functional scorecard index</p>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto custom-scrollbar flex-1">
|
||||
<table className="w-full text-left text-xs font-medium text-gray-600">
|
||||
<thead className="bg-gray-50 text-[10px] text-gray-400 uppercase tracking-wider font-black">
|
||||
<tr>
|
||||
<th className="py-3 px-4">Staff Member</th>
|
||||
<th className="py-3 px-4">Sales Score</th>
|
||||
<th className="py-3 px-4">Activity Score</th>
|
||||
<th className="py-3 px-4">Overall Index</th>
|
||||
<th className="py-3 px-4 text-center">Governance Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100">
|
||||
{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 (
|
||||
<tr key={member.id} className="hover:bg-gray-50/50 transition-colors">
|
||||
<td className="py-3.5 px-4">
|
||||
<span className="font-extrabold text-gray-800 block text-sm">{member.name}</span>
|
||||
<span className="text-[10px] text-gray-400 font-semibold uppercase">{member.role.replace('_', ' ')}</span>
|
||||
</td>
|
||||
<td className="py-3.5 px-4 font-bold text-gray-800 text-sm">
|
||||
{member.revenueScore}/100
|
||||
</td>
|
||||
<td className="py-3.5 px-4 font-bold text-gray-800 text-sm">
|
||||
{member.activityScore}/100
|
||||
</td>
|
||||
<td className="py-3.5 px-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-black text-gray-800 text-sm w-8">{member.score}</span>
|
||||
<div className="w-16 bg-gray-100 h-2 rounded-full overflow-hidden shrink-0">
|
||||
<div
|
||||
className={`h-full rounded-full ${member.score > 75 ? 'bg-emerald-500' : member.score > 50 ? 'bg-amber-500' : 'bg-rose-500'}`}
|
||||
style={{ width: `${member.score}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3.5 px-4 text-center">
|
||||
<span className={`text-[10px] font-black uppercase tracking-wider px-2 py-1 rounded-md ${tagBg}`}>
|
||||
{member.tag.replace('_', ' ')}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Performance Weightage Matrix (70% Sales + 30% Activity) */}
|
||||
<div className="odoo-card p-6 flex flex-col justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-gray-800 mb-1">Performance Weightage Matrix</h3>
|
||||
<p className="text-xs text-gray-500 mb-4">Calculated using 70% Sales performance weight + 30% Activity metrics weight</p>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto flex-1 custom-scrollbar">
|
||||
<table className="w-full text-left text-xs font-semibold text-gray-600">
|
||||
<thead className="bg-gray-50 text-[10px] text-gray-400 uppercase tracking-widest">
|
||||
<tr>
|
||||
<th className="py-2.5 px-2">Staff</th>
|
||||
<th className="py-2.5 px-2 text-right">Sales (70)</th>
|
||||
<th className="py-2.5 px-2 text-right">Act. (30)</th>
|
||||
<th className="py-2.5 px-2 text-right">Weighted</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100">
|
||||
{performanceWeightage.map(pw => (
|
||||
<tr key={pw.id} className="hover:bg-gray-50">
|
||||
<td className="py-2.5 px-2 font-bold text-gray-800">{pw.name}</td>
|
||||
<td className="py-2.5 px-2 text-right text-gray-600 font-medium">{pw.weightedSales}</td>
|
||||
<td className="py-2.5 px-2 text-right text-gray-600 font-medium">{pw.weightedActivity}</td>
|
||||
<td className="py-2.5 px-2 text-right text-odoo-primary font-black text-sm">{pw.totalWeighted}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Activities Analysis & Lead split */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
{/* Activities scheduled vs completed */}
|
||||
<div className="odoo-card p-6 flex flex-col justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-gray-800 mb-1">Activities Completion Index</h3>
|
||||
<p className="text-xs text-gray-500 mb-6">Detailed scheduled vs completed tasks by key sales activity categories</p>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto flex-1 custom-scrollbar">
|
||||
<table className="w-full text-left text-xs font-medium text-gray-600">
|
||||
<thead className="bg-gray-50 text-[10px] text-gray-400 uppercase tracking-wider font-extrabold">
|
||||
<tr>
|
||||
<th className="py-3 px-4">Activity Category</th>
|
||||
<th className="py-3 px-4 text-center">Scheduled</th>
|
||||
<th className="py-3 px-4 text-center">Completed</th>
|
||||
<th className="py-3 px-4 text-right">Completion Rate</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100">
|
||||
{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 (
|
||||
<tr key={act.type} className="hover:bg-gray-50/50">
|
||||
<td className="py-3.5 px-4 font-bold text-gray-800 uppercase tracking-wide">
|
||||
{act.type.replace('_', ' ')}
|
||||
</td>
|
||||
<td className="py-3.5 px-4 text-center font-bold text-gray-700 text-sm">
|
||||
{act.scheduled}
|
||||
</td>
|
||||
<td className="py-3.5 px-4 text-center font-bold text-emerald-600 text-sm">
|
||||
{act.completed}
|
||||
</td>
|
||||
<td className="py-3.5 px-4 text-right">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<span className="font-extrabold text-gray-800 text-sm">{act.completionRate}%</span>
|
||||
<div className="w-12 bg-gray-100 h-1.5 rounded-full overflow-hidden shrink-0">
|
||||
<div
|
||||
className={`h-full rounded-full ${progressColor}`}
|
||||
style={{ width: `${act.completionRate}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Lead vs Shared Conversion Analysis */}
|
||||
<div className="odoo-card p-6 flex flex-col justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-gray-800 mb-1">Collaboration vs Direct Funnel</h3>
|
||||
<p className="text-xs text-gray-500 mb-6">Comparing conversions of single-owner accounts against shared collaborator contracts</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 flex-1 items-center">
|
||||
{/* Own Business */}
|
||||
<div className="p-4 bg-purple-50/50 border border-purple-100 rounded-2xl text-center space-y-2 hover:shadow-sm transition-all duration-200">
|
||||
<span className="text-[10px] text-odoo-primary uppercase font-extrabold tracking-widest">Own Business</span>
|
||||
<div className="py-1">
|
||||
<span className="text-2xl font-black text-gray-800 block">{formatLakhs(leadVsShared.own.value)}</span>
|
||||
<span className="text-xs text-gray-400 font-semibold block mt-0.5">{leadVsShared.own.count} closures</span>
|
||||
</div>
|
||||
<div className="inline-flex items-center gap-1 px-3 py-1 bg-white text-odoo-primary rounded-xl text-xs font-black shadow-sm">
|
||||
<CheckCircle className="w-3.5 h-3.5" />
|
||||
{leadVsShared.own.conversionRate}% Rate
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Shared Business */}
|
||||
<div className="p-4 bg-teal-50/50 border border-teal-100 rounded-2xl text-center space-y-2 hover:shadow-sm transition-all duration-200">
|
||||
<span className="text-[10px] text-odoo-secondary uppercase font-extrabold tracking-widest">Shared Business</span>
|
||||
<div className="py-1">
|
||||
<span className="text-2xl font-black text-gray-800 block">{formatLakhs(leadVsShared.shared.value)}</span>
|
||||
<span className="text-xs text-gray-400 font-semibold block mt-0.5">{leadVsShared.shared.count} closures</span>
|
||||
</div>
|
||||
<div className="inline-flex items-center gap-1 px-3 py-1 bg-white text-odoo-secondary rounded-xl text-xs font-black shadow-sm">
|
||||
<Users className="w-3.5 h-3.5" />
|
||||
{leadVsShared.shared.conversionRate}% Rate
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 text-center bg-gray-50 p-2.5 rounded-xl border border-gray-150 text-xs font-semibold text-gray-500">
|
||||
Total enquiries analyzed in this period: <strong className="text-gray-700 text-sm ml-1">{leadVsShared.totalEnquiries}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Revenue Contribution Tracker */}
|
||||
<div className="odoo-card p-6">
|
||||
<h3 className="text-lg font-bold text-gray-800 mb-1">Collaborative Revenue Ledger</h3>
|
||||
<p className="text-xs text-gray-500 mb-4">Detailed contract transaction list with 50/50 revenue attribution split metrics</p>
|
||||
|
||||
<div className="overflow-x-auto custom-scrollbar">
|
||||
<table className="w-full text-left text-xs font-medium text-gray-600">
|
||||
<thead className="bg-gray-50 text-[10px] text-gray-400 uppercase tracking-wider font-extrabold">
|
||||
<tr>
|
||||
<th className="py-3 px-4">Lead Origin / Creator</th>
|
||||
<th className="py-3 px-4">Closing Resource</th>
|
||||
<th className="py-3 px-4">Full Value</th>
|
||||
<th className="py-3 px-4 text-center">Structure</th>
|
||||
<th className="py-3 px-4 text-right">Attributed Shares (50/50 split)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100">
|
||||
{revenueContribution.length > 0 ? revenueContribution.map((rc, idx) => (
|
||||
<tr key={idx} className="hover:bg-gray-50/50">
|
||||
<td className="py-3 px-4 font-bold text-gray-700">
|
||||
{rc.leadOwner}
|
||||
</td>
|
||||
<td className="py-3 px-4 font-bold text-gray-700">
|
||||
{rc.closingOwner}
|
||||
</td>
|
||||
<td className="py-3 px-4 font-black text-gray-800 text-sm">
|
||||
{formatCurrency(rc.value)}
|
||||
</td>
|
||||
<td className="py-3 px-4 text-center">
|
||||
{rc.isOwn ? (
|
||||
<span className="text-[10px] font-black text-purple-700 bg-purple-50 px-2 py-0.5 rounded border border-purple-100">
|
||||
SOLE
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-[10px] font-black text-teal-700 bg-teal-50 px-2 py-0.5 rounded border border-teal-100 animate-pulse">
|
||||
SHARED SPLIT
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-3 px-4 text-right text-gray-500 font-semibold">
|
||||
{rc.isOwn ? (
|
||||
<span className="text-gray-800 font-bold">{formatCurrency(rc.value)} full</span>
|
||||
) : (
|
||||
<span>
|
||||
Creator: <strong className="text-gray-700 font-bold">{formatCurrency(rc.leadOwnerShare)}</strong> | Closer: <strong className="text-gray-700 font-bold">{formatCurrency(rc.closingOwnerShare)}</strong>
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
)) : (
|
||||
<tr>
|
||||
<td colSpan={5} className="py-8 text-center text-gray-400 italic text-sm">
|
||||
No won sales transactions reported this month.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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<MyDashboardData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(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 (
|
||||
<div className="flex flex-col items-center justify-center h-96">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-odoo-primary mb-4"></div>
|
||||
<p className="text-gray-500 font-medium">Preparing your sales dashboard...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !data) {
|
||||
return (
|
||||
<div className="odoo-card p-8 text-center max-w-lg mx-auto my-12">
|
||||
<AlertCircle className="w-12 h-12 text-rose-500 mx-auto mb-4" />
|
||||
<h3 className="text-xl font-bold text-gray-800 mb-2">Error Loading Dashboard</h3>
|
||||
<p className="text-gray-600 mb-6">{error || "No data received from server."}</p>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="px-6 py-2 bg-odoo-primary text-white rounded-md font-semibold hover:bg-odoo-accent transition-colors"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="space-y-8 pb-12 animate-fade-in">
|
||||
{/* Header section with greeting */}
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 bg-gradient-to-r from-odoo-primary via-[#875a7b] to-odoo-secondary text-white p-6 rounded-2xl shadow-lg">
|
||||
<div>
|
||||
<h1 className="text-2xl md:text-3xl font-bold tracking-tight">My Sales Dashboard</h1>
|
||||
<p className="text-white/80 text-sm mt-1 flex items-center gap-1.5">
|
||||
<Calendar className="w-4 h-4" />
|
||||
Performance tracking for {currentMonthName} {currentYear}
|
||||
</p>
|
||||
</div>
|
||||
{target && (
|
||||
<div className="flex gap-4 items-center bg-white/10 backdrop-blur-md rounded-xl p-3 border border-white/10">
|
||||
<div className="text-right">
|
||||
<span className="text-xs uppercase tracking-wider text-white/70 block">Monthly Target</span>
|
||||
<span className="text-lg font-bold">{formatCurrency(target.monthly)}</span>
|
||||
</div>
|
||||
<div className="w-px h-8 bg-white/20" />
|
||||
<div className="text-right">
|
||||
<span className="text-xs uppercase tracking-wider text-white/70 block">MTD Achieved</span>
|
||||
<span className="text-lg font-bold text-emerald-300">{formatCurrency(totalActual)}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* KPI Cards Grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{/* Card 1: Achievement % */}
|
||||
<div className="odoo-card p-6 border-l-4 border-emerald-500 relative overflow-hidden group hover:-translate-y-1">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-gray-500 uppercase tracking-wider">Achievement</p>
|
||||
<h3 className="text-3xl font-extrabold text-gray-800 mt-2">{achievementPct}%</h3>
|
||||
</div>
|
||||
<div className="p-3 bg-emerald-50 text-emerald-600 rounded-xl group-hover:bg-emerald-500 group-hover:text-white transition-all duration-300">
|
||||
<Award className="w-6 h-6" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center gap-1.5">
|
||||
<div className="w-full bg-gray-150 h-2 rounded-full overflow-hidden">
|
||||
<div className="bg-emerald-500 h-full rounded-full transition-all duration-500" style={{ width: `${achievementPct}%` }}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Card 2: Remaining Target */}
|
||||
<div className="odoo-card p-6 border-l-4 border-odoo-primary relative overflow-hidden group hover:-translate-y-1">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-gray-500 uppercase tracking-wider">Remaining Target</p>
|
||||
<h3 className="text-2xl font-extrabold text-gray-800 mt-2">{formatCurrency(remaining)}</h3>
|
||||
</div>
|
||||
<div className="p-3 bg-purple-50 text-odoo-primary rounded-xl group-hover:bg-odoo-primary group-hover:text-white transition-all duration-300">
|
||||
<IndianRupee className="w-6 h-6" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-4 font-medium">
|
||||
{remainingPct}% of target left to achieve
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Card 3: Own Sales % */}
|
||||
<div className="odoo-card p-6 border-l-4 border-odoo-accent relative overflow-hidden group hover:-translate-y-1">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-gray-500 uppercase tracking-wider">Own Sales Contribution</p>
|
||||
<h3 className="text-3xl font-extrabold text-gray-800 mt-2">{ownSales.pct}%</h3>
|
||||
</div>
|
||||
<div className="p-3 bg-pink-50 text-odoo-accent rounded-xl group-hover:bg-odoo-accent group-hover:text-white transition-all duration-300">
|
||||
<Users className="w-6 h-6" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-4 font-medium">
|
||||
Value: <span className="font-semibold text-gray-700">{formatLakhs(ownSales.value)}</span> ({ownSales.count} deals)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Card 4: Shared Sales % */}
|
||||
<div className="odoo-card p-6 border-l-4 border-odoo-secondary relative overflow-hidden group hover:-translate-y-1">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-gray-500 uppercase tracking-wider">Shared Sales Contribution</p>
|
||||
<h3 className="text-3xl font-extrabold text-gray-800 mt-2">{sharedSales.pct}%</h3>
|
||||
</div>
|
||||
<div className="p-3 bg-teal-50 text-odoo-secondary rounded-xl group-hover:bg-odoo-secondary group-hover:text-white transition-all duration-300">
|
||||
<TrendingUp className="w-6 h-6" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-4 font-medium">
|
||||
Value: <span className="font-semibold text-gray-700">{formatLakhs(sharedSales.value)}</span> ({sharedSales.count} deals)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Target Progress Bar Detail */}
|
||||
<div className="odoo-card p-6 relative overflow-hidden">
|
||||
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-4 gap-2">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-gray-800 flex items-center gap-2">
|
||||
<Target className="w-5 h-5 text-odoo-primary" />
|
||||
Target vs Achieved Progression
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500 mt-0.5">Real-time status of your targets for the month</p>
|
||||
</div>
|
||||
<span className="text-sm font-bold text-emerald-600 bg-emerald-50 px-3 py-1.5 rounded-lg border border-emerald-100 flex items-center gap-1 animate-pulse">
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
{progressPercent.toFixed(1)}% Completed
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="w-full bg-gray-100 h-6 rounded-full overflow-hidden relative shadow-inner p-1">
|
||||
<div
|
||||
className="h-full rounded-full bg-gradient-to-r from-odoo-primary via-[#875a7b] to-emerald-500 transition-all duration-700 ease-out flex items-center justify-end pr-2 min-w-[2rem]"
|
||||
style={{ width: `${progressPercent}%` }}
|
||||
>
|
||||
{progressPercent > 8 && (
|
||||
<span className="text-[10px] font-extrabold text-white leading-none shadow-sm drop-shadow-sm select-none">
|
||||
{progressPercent.toFixed(0)}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between text-xs font-semibold text-gray-500 mt-2 px-1">
|
||||
<span>MTD Achieved: <strong className="text-emerald-600 text-sm">{formatCurrency(totalActual)}</strong></span>
|
||||
<span>Monthly Target: <strong className="text-gray-700 text-sm">{formatCurrency(totalExpected)}</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Charts Section: Weekly Bar + Own/Shared Pie */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Grouped Weekly expected vs actual */}
|
||||
<div className="odoo-card p-6 lg:col-span-2 flex flex-col justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-gray-800">Week-wise Expected vs Actual Value</h3>
|
||||
<p className="text-xs text-gray-500 mb-6">Comparison of weekly targets against actual revenue contributions</p>
|
||||
</div>
|
||||
<div className="h-72">
|
||||
<Bar options={weeklyChartOptions} data={weeklyChartData} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Own vs Shared Doughnut */}
|
||||
<div className="odoo-card p-6 flex flex-col justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-gray-800">Sales Attribution</h3>
|
||||
<p className="text-xs text-gray-500 mb-6">Split between sole closure vs collaborative sales split</p>
|
||||
</div>
|
||||
<div className="h-52 flex items-center justify-center relative">
|
||||
<Doughnut options={salesTypeOptions} data={salesTypeData} />
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none mt-6">
|
||||
<span className="text-2xl font-black text-gray-800">
|
||||
{formatLakhs(ownSales.value + sharedSales.value)}
|
||||
</span>
|
||||
<span className="text-xs text-gray-400 font-semibold uppercase tracking-wider mt-0.5">Total Sales</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 space-y-3">
|
||||
<div className="flex justify-between items-center text-sm border-t border-gray-100 pt-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 bg-odoo-primary rounded-full" />
|
||||
<span className="font-semibold text-gray-600">Own Sales</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span className="font-bold text-gray-800 block">{formatCurrency(ownSales.value)}</span>
|
||||
<span className="text-xs text-gray-400 font-medium">{ownSales.count} Deals • {ownSales.pct}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between items-center text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 bg-odoo-secondary rounded-full" />
|
||||
<span className="font-semibold text-gray-600">Shared Sales</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span className="font-bold text-gray-800 block">{formatCurrency(sharedSales.value)}</span>
|
||||
<span className="text-xs text-gray-400 font-medium">{sharedSales.count} Deals • {sharedSales.pct}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stage-wise Pipeline & Target Breakdown Grid */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Stage-wise pipeline count & value */}
|
||||
<div className="odoo-card p-6 lg:col-span-2">
|
||||
<h3 className="text-lg font-bold text-gray-800 mb-1">Stage-wise Pipeline Count & Value</h3>
|
||||
<p className="text-xs text-gray-500 mb-6">Breakdown of opportunities in active stages of the funnel</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
{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 (
|
||||
<div key={item.stage} className="group flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2 p-3 hover:bg-gray-50 rounded-xl transition-all duration-200">
|
||||
<div className="flex items-center gap-3 w-40 shrink-0">
|
||||
<div className={`w-3.5 h-3.5 rounded-full ${barColor} shadow-sm`} />
|
||||
<span className="font-extrabold text-sm text-gray-700 tracking-wide uppercase">{item.stage}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 w-full flex items-center gap-4">
|
||||
<div className="flex-1 bg-gray-100 h-3 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full transition-all duration-500 ${barColor}`}
|
||||
style={{ width: `${Math.max(4, percent)}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 w-44 shrink-0 text-right justify-end font-semibold text-sm">
|
||||
<span className="text-gray-400 text-xs font-bold">{item.count} deals</span>
|
||||
<span className={`w-24 ${textColor} font-black`}>{formatLakhs(item.value)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Target Summary Widget */}
|
||||
<div className="odoo-card p-6 flex flex-col justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-gray-800 mb-1">Target Summary</h3>
|
||||
<p className="text-xs text-gray-500 mb-6">Metrics required to meet goals this month</p>
|
||||
</div>
|
||||
|
||||
{target ? (
|
||||
<div className="space-y-4 flex-1 flex flex-col justify-center">
|
||||
<div className="p-3 bg-gray-50 rounded-xl border border-gray-100 flex justify-between items-center hover:shadow-sm transition-all duration-200">
|
||||
<div>
|
||||
<span className="text-xs text-gray-400 font-bold uppercase tracking-wider block">Monthly Target</span>
|
||||
<span className="text-base font-extrabold text-gray-700">{formatCurrency(target.monthly)}</span>
|
||||
</div>
|
||||
<span className="text-xs font-bold text-odoo-primary bg-purple-50 px-2 py-1 rounded">100%</span>
|
||||
</div>
|
||||
|
||||
<div className="p-3 bg-gray-50 rounded-xl border border-gray-100 flex justify-between items-center hover:shadow-sm transition-all duration-200">
|
||||
<div>
|
||||
<span className="text-xs text-gray-400 font-bold uppercase tracking-wider block">Minimum Target</span>
|
||||
<span className="text-base font-extrabold text-gray-700">{formatCurrency(target.minimum)}</span>
|
||||
</div>
|
||||
<span className="text-xs font-bold text-amber-600 bg-amber-50 px-2 py-1 rounded">Min Threshold</span>
|
||||
</div>
|
||||
|
||||
<div className="p-3 bg-gray-50 rounded-xl border border-gray-100 flex justify-between items-center hover:shadow-sm transition-all duration-200">
|
||||
<div>
|
||||
<span className="text-xs text-gray-400 font-bold uppercase tracking-wider block">Weekly Target</span>
|
||||
<span className="text-base font-extrabold text-gray-700">{formatCurrency(target.weekly)}</span>
|
||||
</div>
|
||||
<span className="text-xs font-bold text-odoo-secondary bg-teal-50 px-2 py-1 rounded">4 Weeks split</span>
|
||||
</div>
|
||||
|
||||
<div className="p-3 bg-gray-50 rounded-xl border border-gray-100 flex justify-between items-center hover:shadow-sm transition-all duration-200">
|
||||
<div>
|
||||
<span className="text-xs text-gray-400 font-bold uppercase tracking-wider block">Daily Lead Target</span>
|
||||
<span className="text-base font-extrabold text-gray-700">{target.dailyLead} Leads / Day</span>
|
||||
</div>
|
||||
<span className="text-xs font-bold text-indigo-600 bg-indigo-50 px-2 py-1 rounded">Volume metric</span>
|
||||
</div>
|
||||
|
||||
{(target.requiredClosures || target.requiredDemos) && (
|
||||
<div className="mt-2 pt-4 border-t border-dashed border-gray-200 grid grid-cols-2 gap-3 text-center">
|
||||
{target.requiredClosures && (
|
||||
<div className="p-2.5 bg-blue-50/50 rounded-xl border border-blue-100">
|
||||
<span className="text-[10px] text-blue-600 uppercase font-black block">Closures Needed</span>
|
||||
<span className="text-lg font-black text-blue-700 mt-1 block">{target.requiredClosures}</span>
|
||||
</div>
|
||||
)}
|
||||
{target.requiredDemos && (
|
||||
<div className="p-2.5 bg-pink-50/50 rounded-xl border border-pink-100">
|
||||
<span className="text-[10px] text-pink-600 uppercase font-black block">Demos Needed</span>
|
||||
<span className="text-lg font-black text-pink-700 mt-1 block">{target.requiredDemos}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 flex flex-col items-center justify-center py-6 text-center">
|
||||
<AlertCircle className="w-10 h-10 text-amber-500 mb-2" />
|
||||
<span className="text-sm font-semibold text-gray-700 block">No target defined</span>
|
||||
<span className="text-xs text-gray-400 mt-1 max-w-[200px]">Ask your manager to set your monthly goals for {currentMonthName}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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<any[]>([]);
|
||||
const [products, setProducts] = useState<any[]>([]);
|
||||
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 (
|
||||
<div className="h-full flex flex-col bg-white">
|
||||
|
|
@ -590,12 +605,31 @@ export default function OpportunityBoard() {
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-6">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="relative w-64">
|
||||
<Search size={14} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search client, email or company..."
|
||||
className="w-full pl-9 pr-8 py-1.5 bg-gray-50 border border-gray-200 rounded-full focus:bg-white focus:ring-1 focus:ring-odoo-primary focus:border-odoo-primary outline-none transition-all text-[12px] font-medium placeholder:text-gray-400"
|
||||
value={clientSearchTerm}
|
||||
onChange={(e) => setClientSearchTerm(e.target.value)}
|
||||
/>
|
||||
{clientSearchTerm && (
|
||||
<button
|
||||
onClick={() => setClientSearchTerm('')}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 transition-colors"
|
||||
>
|
||||
<X size={12} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2 bg-gray-50 border border-gray-200 px-3 py-1.5 rounded-full w-64">
|
||||
<TrendingUp size={14} className="text-gray-400" />
|
||||
<span className="text-[12px] font-bold text-gray-400 uppercase tracking-tighter">Total Pipeline:</span>
|
||||
<span className="text-[14px] font-extrabold text-odoo-primary">
|
||||
₹{items.reduce((sum, i) => sum + i.value, 0).toLocaleString()}
|
||||
₹{filteredItems.reduce((sum, i) => sum + i.value, 0).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Reference in New Issue