parent
9a875aea9f
commit
107126b1f4
|
|
@ -1,117 +1,14 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React 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 { useAuth } from '@/context/AuthContext';
|
import { useAuth } from '@/context/AuthContext';
|
||||||
import { formatDistanceToNow } from 'date-fns';
|
import MyDashboard from './MyDashboard';
|
||||||
import FunnelAnalytics from './FunnelAnalytics';
|
import ManagerDashboard from './ManagerDashboard';
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function DashboardOverview() {
|
export default function DashboardOverview() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const [stats, setStats] = useState<DashboardStats | null>(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
if (!user) {
|
||||||
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) {
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-64">
|
<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>
|
<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 = [
|
return isManager ? <ManagerDashboard /> : <MyDashboard />;
|
||||||
{
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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";
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
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 {
|
import {
|
||||||
DndContext,
|
DndContext,
|
||||||
closestCenter,
|
closestCenter,
|
||||||
|
|
@ -36,6 +36,7 @@ interface Opportunity {
|
||||||
client: {
|
client: {
|
||||||
name: string;
|
name: string;
|
||||||
id: string;
|
id: string;
|
||||||
|
email?: string;
|
||||||
companyName?: string;
|
companyName?: string;
|
||||||
contactName?: string;
|
contactName?: string;
|
||||||
closingProbability?: number;
|
closingProbability?: number;
|
||||||
|
|
@ -280,6 +281,20 @@ export default function OpportunityBoard() {
|
||||||
const [assignees, setAssignees] = useState<any[]>([]);
|
const [assignees, setAssignees] = useState<any[]>([]);
|
||||||
const [products, setProducts] = useState<any[]>([]);
|
const [products, setProducts] = useState<any[]>([]);
|
||||||
const [loadingAssignees, setLoadingAssignees] = useState(false);
|
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) => {
|
const getFileUrl = (url: string) => {
|
||||||
if (!url) return '#';
|
if (!url) return '#';
|
||||||
|
|
@ -570,7 +585,7 @@ export default function OpportunityBoard() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const stages = ['LEAD', 'QUALIFIED', 'POTENTIAL', 'SALES'];
|
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 (
|
return (
|
||||||
<div className="h-full flex flex-col bg-white">
|
<div className="h-full flex flex-col bg-white">
|
||||||
|
|
@ -590,12 +605,31 @@ export default function OpportunityBoard() {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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">
|
<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" />
|
<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-[12px] font-bold text-gray-400 uppercase tracking-tighter">Total Pipeline:</span>
|
||||||
<span className="text-[14px] font-extrabold text-odoo-primary">
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -614,7 +648,7 @@ export default function OpportunityBoard() {
|
||||||
key={stage}
|
key={stage}
|
||||||
id={stage}
|
id={stage}
|
||||||
title={STAGE_CONFIG[stage].title}
|
title={STAGE_CONFIG[stage].title}
|
||||||
items={items.filter((i) => i.stage === stage)}
|
items={filteredItems.filter((i) => i.stage === stage)}
|
||||||
totalValue={getColumnTotal(stage)}
|
totalValue={getColumnTotal(stage)}
|
||||||
onAddClick={handleCreateClick}
|
onAddClick={handleCreateClick}
|
||||||
onEditClick={handleEditClick}
|
onEditClick={handleEditClick}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue