changes till 01/06/2026

changes till 01/06/2026
main
Manu Krishna 2026-06-01 11:41:52 +05:30
parent 9a875aea9f
commit 107126b1f4
4 changed files with 1357 additions and 480 deletions

View File

@ -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} &bull; {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> &bull; {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 />;
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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}