335 lines
19 KiB
JavaScript
335 lines
19 KiB
JavaScript
import React from "react";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Progress } from "@/components/ui/progress";
|
|
import {
|
|
TrendingUp, TrendingDown, DollarSign, Users, Target,
|
|
CheckCircle, AlertTriangle, Clock, BarChart3, PieChart,
|
|
ArrowUpRight, Zap, Shield, Star, Activity, Calendar,
|
|
Package, Award, MapPin, Building2
|
|
} from "lucide-react";
|
|
import { PieChart as RechartsPie, Pie, Cell, ResponsiveContainer, Tooltip, Legend } from "recharts";
|
|
|
|
export default function DynamicSavingsDashboard({ metrics, projections, timeRange, userRole }) {
|
|
const getTimeLabel = () => {
|
|
switch (timeRange) {
|
|
case "7days": return "7-Day";
|
|
case "30days": return "30-Day";
|
|
case "quarter": return "Quarterly";
|
|
case "year": return "Annual";
|
|
default: return "30-Day";
|
|
}
|
|
};
|
|
|
|
const getProjectedSavings = () => {
|
|
switch (timeRange) {
|
|
case "7days": return projections.sevenDays;
|
|
case "30days": return projections.thirtyDays;
|
|
case "quarter": return projections.quarter;
|
|
case "year": return projections.year;
|
|
default: return projections.thirtyDays;
|
|
}
|
|
};
|
|
|
|
// Role-specific content
|
|
const getRoleContent = () => {
|
|
switch (userRole) {
|
|
case "procurement":
|
|
return {
|
|
mainTitle: "Vendor Network Performance",
|
|
mainSubtitle: "Optimize your preferred vendor network",
|
|
chartTitle: "Vendor Tier Distribution",
|
|
chartData: [
|
|
{ name: "Preferred Vendors", value: 40, color: "#22c55e" },
|
|
{ name: "Approved Vendors", value: 35, color: "#0A39DF" },
|
|
{ name: "Standard Vendors", value: 25, color: "#f59e0b" },
|
|
],
|
|
kpis: [
|
|
{ label: "Avg Vendor Score", current: `${(metrics.avgReliability + 5).toFixed(0)}%`, optimized: "95%", savings: `+${(95 - metrics.avgReliability - 5).toFixed(0)}%`, trend: "up", icon: Award },
|
|
{ label: "Contract Compliance", current: `${metrics.contractedRatio.toFixed(0)}%`, optimized: "90%", savings: `+${(90 - metrics.contractedRatio).toFixed(0)}%`, trend: "up", icon: Shield },
|
|
{ label: "Rate Variance", current: `$${metrics.avgNonContractedRate.toFixed(2)}`, optimized: `$${metrics.avgContractedRate.toFixed(2)}`, savings: `-$${(metrics.avgNonContractedRate - metrics.avgContractedRate).toFixed(2)}`, trend: "down", icon: DollarSign },
|
|
{ label: "SLA Adherence", current: `${metrics.fillRate.toFixed(0)}%`, optimized: "98%", savings: `+${(98 - metrics.fillRate).toFixed(0)}%`, trend: "up", icon: CheckCircle },
|
|
],
|
|
actions: [
|
|
{ priority: "high", action: "Upgrade 3 high-performing Standard vendors to Approved tier", impact: "Better rates, guaranteed capacity", deadline: "This week" },
|
|
{ priority: "high", action: "Renegotiate rates with top 5 Preferred vendors", impact: `$${(getProjectedSavings() * 0.3).toLocaleString()} potential savings`, deadline: "This month" },
|
|
{ priority: "medium", action: "Review vendors below 85% SLA adherence", impact: "Improve network reliability", deadline: "Next 2 weeks" },
|
|
],
|
|
};
|
|
|
|
case "operator":
|
|
return {
|
|
mainTitle: "Enterprise Operational Efficiency",
|
|
mainSubtitle: "Cross-sector performance optimization",
|
|
chartTitle: "Spend by Sector",
|
|
chartData: [
|
|
{ name: "Food Service", value: 45, color: "#0A39DF" },
|
|
{ name: "Events", value: 30, color: "#8b5cf6" },
|
|
{ name: "Facilities", value: 15, color: "#06b6d4" },
|
|
{ name: "Other", value: 10, color: "#f59e0b" },
|
|
],
|
|
kpis: [
|
|
{ label: "Fill Rate", current: `${metrics.fillRate.toFixed(0)}%`, optimized: "98%", savings: `+${(98 - metrics.fillRate).toFixed(0)}%`, trend: "up", icon: Target },
|
|
{ label: "Labor Utilization", current: `${metrics.avgReliability.toFixed(0)}%`, optimized: "95%", savings: `+${(95 - metrics.avgReliability).toFixed(0)}%`, trend: "up", icon: Users },
|
|
{ label: "Cost Efficiency", current: `$${(metrics.totalSpend / Math.max(metrics.completedOrders, 1)).toFixed(0)}`, optimized: `$${((metrics.totalSpend / Math.max(metrics.completedOrders, 1)) * 0.85).toFixed(0)}`, savings: "-15%", trend: "down", icon: DollarSign },
|
|
{ label: "Overtime Rate", current: "12%", optimized: "5%", savings: "-7%", trend: "down", icon: Clock },
|
|
],
|
|
actions: [
|
|
{ priority: "high", action: "Balance workforce across underperforming sectors", impact: "Improve fill rate by 8%", deadline: "This week" },
|
|
{ priority: "high", action: "Implement predictive scheduling for peak periods", impact: "Reduce overtime by 40%", deadline: "Next 2 weeks" },
|
|
{ priority: "medium", action: "Cross-train staff for multi-sector flexibility", impact: "Increase utilization 15%", deadline: "This quarter" },
|
|
],
|
|
};
|
|
|
|
case "sector":
|
|
return {
|
|
mainTitle: "Location Performance",
|
|
mainSubtitle: "Your site's staffing efficiency",
|
|
chartTitle: "Position Coverage",
|
|
chartData: [
|
|
{ name: "Filled Positions", value: metrics.fillRate, color: "#22c55e" },
|
|
{ name: "Open Gaps", value: 100 - metrics.fillRate, color: "#ef4444" },
|
|
],
|
|
kpis: [
|
|
{ label: "Position Fill Rate", current: `${metrics.fillRate.toFixed(0)}%`, optimized: "100%", savings: `+${(100 - metrics.fillRate).toFixed(0)}%`, trend: "up", icon: Target },
|
|
{ label: "Staff Attendance", current: `${(100 - metrics.noShowRate).toFixed(0)}%`, optimized: "99%", savings: `+${(99 - (100 - metrics.noShowRate)).toFixed(0)}%`, trend: "up", icon: CheckCircle },
|
|
{ label: "Shift Coverage", current: `${metrics.avgReliability.toFixed(0)}%`, optimized: "98%", savings: `+${(98 - metrics.avgReliability).toFixed(0)}%`, trend: "up", icon: Calendar },
|
|
{ label: "Response Time", current: "4 hrs", optimized: "1 hr", savings: "-3 hrs", trend: "down", icon: Clock },
|
|
],
|
|
actions: [
|
|
{ priority: "high", action: "Fill 3 critical morning shift gaps", impact: "100% coverage for peak hours", deadline: "Tomorrow" },
|
|
{ priority: "high", action: "Request backup staff for weekend events", impact: "Prevent no-show impact", deadline: "This week" },
|
|
{ priority: "medium", action: "Train cross-functional backup team", impact: "Reduce gap response time", deadline: "This month" },
|
|
],
|
|
};
|
|
|
|
case "client":
|
|
return {
|
|
mainTitle: "Your Event Coverage & Savings",
|
|
mainSubtitle: "Maximize staffing quality while reducing costs",
|
|
chartTitle: "Event Fulfillment",
|
|
chartData: [
|
|
{ name: "Fully Staffed", value: metrics.fillRate, color: "#22c55e" },
|
|
{ name: "Partial Coverage", value: Math.max(0, 100 - metrics.fillRate - 5), color: "#f59e0b" },
|
|
{ name: "Gaps", value: Math.min(5, 100 - metrics.fillRate), color: "#ef4444" },
|
|
],
|
|
kpis: [
|
|
{ label: "Event Coverage", current: `${metrics.fillRate.toFixed(0)}%`, optimized: "100%", savings: `+${(100 - metrics.fillRate).toFixed(0)}%`, trend: "up", icon: Calendar },
|
|
{ label: "Staff Quality", current: `${metrics.avgReliability.toFixed(0)}%`, optimized: "95%", savings: `+${(95 - metrics.avgReliability).toFixed(0)}%`, trend: "up", icon: Star },
|
|
{ label: "Cost Savings", current: `$${metrics.avgNonContractedRate.toFixed(0)}/hr`, optimized: `$${metrics.avgContractedRate.toFixed(0)}/hr`, savings: `-$${(metrics.avgNonContractedRate - metrics.avgContractedRate).toFixed(0)}/hr`, trend: "down", icon: DollarSign },
|
|
{ label: "On-Time Arrival", current: `${(100 - metrics.noShowRate).toFixed(0)}%`, optimized: "99%", savings: `+${(99 - (100 - metrics.noShowRate)).toFixed(0)}%`, trend: "up", icon: Clock },
|
|
],
|
|
actions: [
|
|
{ priority: "high", action: "Book preferred vendors for upcoming events", impact: `Save $${(getProjectedSavings() * 0.2).toLocaleString()} on rates`, deadline: "Before next event" },
|
|
{ priority: "medium", action: "Request top-rated staff from past events", impact: "Improve service quality 20%", deadline: "Next booking" },
|
|
{ priority: "medium", action: "Bundle multiple events for volume discount", impact: "Additional 10% savings", deadline: "This month" },
|
|
],
|
|
};
|
|
|
|
case "vendor":
|
|
return {
|
|
mainTitle: "Your Competitive Performance",
|
|
mainSubtitle: "Stand out in the vendor network",
|
|
chartTitle: "Your Performance vs. Network",
|
|
chartData: [
|
|
{ name: "Your Score", value: metrics.avgReliability, color: "#0A39DF" },
|
|
{ name: "Network Avg", value: 100 - metrics.avgReliability, color: "#e2e8f0" },
|
|
],
|
|
kpis: [
|
|
{ label: "Fill Rate", current: `${metrics.fillRate.toFixed(0)}%`, optimized: "99%", savings: `+${(99 - metrics.fillRate).toFixed(0)}%`, trend: "up", icon: Target },
|
|
{ label: "Team Reliability", current: `${metrics.avgReliability.toFixed(0)}%`, optimized: "98%", savings: `+${(98 - metrics.avgReliability).toFixed(0)}%`, trend: "up", icon: Users },
|
|
{ label: "Client Value", current: `$${(metrics.avgNonContractedRate - metrics.avgContractedRate).toFixed(2)}/hr`, optimized: `$${((metrics.avgNonContractedRate - metrics.avgContractedRate) * 1.2).toFixed(2)}/hr`, savings: "+20%", trend: "up", icon: Zap },
|
|
{ label: "Response Time", current: "2 hrs", optimized: "30 min", savings: "-1.5 hrs", trend: "down", icon: Clock },
|
|
],
|
|
actions: [
|
|
{ priority: "high", action: "Improve response time to order requests", impact: "Win 15% more orders", deadline: "Immediate" },
|
|
{ priority: "high", action: "Reduce no-show rate below 2%", impact: "Upgrade to Preferred tier", deadline: "This month" },
|
|
{ priority: "medium", action: "Expand certified workforce pool", impact: "Access premium assignments", deadline: "This quarter" },
|
|
],
|
|
};
|
|
|
|
default: // admin
|
|
return {
|
|
mainTitle: "Platform-Wide Savings",
|
|
mainSubtitle: "Transform labor spend into strategic advantage",
|
|
chartTitle: "Labor Spend Mix",
|
|
chartData: [
|
|
{ name: "Contracted Labor", value: metrics.contractedSpend, color: "#22c55e" },
|
|
{ name: "Non-Contracted", value: metrics.nonContractedSpend, color: "#ef4444" },
|
|
],
|
|
kpis: [
|
|
{ label: "Cost per Hour", current: `$${metrics.avgNonContractedRate.toFixed(2)}`, optimized: `$${metrics.avgContractedRate.toFixed(2)}`, savings: `-$${(metrics.avgNonContractedRate - metrics.avgContractedRate).toFixed(2)}`, trend: "down", icon: DollarSign },
|
|
{ label: "Fill Rate", current: `${metrics.fillRate.toFixed(1)}%`, optimized: "98%", savings: `+${(98 - metrics.fillRate).toFixed(1)}%`, trend: "up", icon: Target },
|
|
{ label: "No-Show Rate", current: `${metrics.noShowRate.toFixed(1)}%`, optimized: "1%", savings: `-${(metrics.noShowRate - 1).toFixed(1)}%`, trend: "down", icon: AlertTriangle },
|
|
{ label: "Reliability Score", current: `${metrics.avgReliability.toFixed(0)}%`, optimized: "95%", savings: `+${(95 - metrics.avgReliability).toFixed(0)}%`, trend: "up", icon: Shield },
|
|
],
|
|
actions: [
|
|
{ priority: "high", action: "Convert 15 high-volume gig workers to preferred vendor", impact: `$${(getProjectedSavings() * 0.25).toLocaleString()} savings`, deadline: "This week" },
|
|
{ priority: "high", action: "Negotiate volume discount with top 3 vendors", impact: `$${(getProjectedSavings() * 0.2).toLocaleString()} savings`, deadline: "Next 2 weeks" },
|
|
{ priority: "medium", action: "Reduce overtime through better scheduling", impact: `$${(getProjectedSavings() * 0.15).toLocaleString()} savings`, deadline: "This month" },
|
|
],
|
|
};
|
|
}
|
|
};
|
|
|
|
const content = getRoleContent();
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Main Summary */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
<Card className="lg:col-span-2 bg-gradient-to-br from-[#0A39DF] to-[#1C323E] text-white border-0">
|
|
<CardContent className="p-6">
|
|
<div className="flex items-start justify-between mb-6">
|
|
<div>
|
|
<p className="text-blue-200 text-sm font-medium mb-1">{content.mainTitle}</p>
|
|
<p className="text-4xl font-bold">${getProjectedSavings().toLocaleString(undefined, { maximumFractionDigits: 0 })}</p>
|
|
<p className="text-blue-200 mt-2">{content.mainSubtitle}</p>
|
|
</div>
|
|
<div className="w-16 h-16 bg-white/20 rounded-2xl flex items-center justify-center">
|
|
<TrendingUp className="w-8 h-8 text-white" />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
{content.kpis.slice(0, 4).map((kpi, idx) => (
|
|
<div key={idx} className="bg-white/10 rounded-lg p-3">
|
|
<p className="text-blue-200 text-xs">{kpi.label}</p>
|
|
<p className="text-xl font-bold">{kpi.current}</p>
|
|
<p className="text-xs text-green-300">{kpi.savings}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="text-base flex items-center gap-2">
|
|
<PieChart className="w-5 h-5 text-slate-400" />
|
|
{content.chartTitle}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="h-48">
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<RechartsPie>
|
|
<Pie
|
|
data={content.chartData}
|
|
cx="50%"
|
|
cy="50%"
|
|
innerRadius={50}
|
|
outerRadius={70}
|
|
paddingAngle={5}
|
|
dataKey="value"
|
|
>
|
|
{content.chartData.map((entry, index) => (
|
|
<Cell key={`cell-${index}`} fill={entry.color} />
|
|
))}
|
|
</Pie>
|
|
<Tooltip formatter={(value) => typeof value === 'number' && value > 100 ? `$${value.toLocaleString()}` : `${value}%`} />
|
|
</RechartsPie>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
<div className="flex flex-wrap justify-center gap-3 mt-2">
|
|
{content.chartData.map((item, idx) => (
|
|
<div key={idx} className="flex items-center gap-2 text-xs">
|
|
<div className="w-3 h-3 rounded-full" style={{ backgroundColor: item.color }} />
|
|
<span className="text-slate-600">{item.name}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* KPI Comparison */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Activity className="w-5 h-5 text-blue-500" />
|
|
Performance Optimization Metrics
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
{content.kpis.map((kpi, idx) => {
|
|
const Icon = kpi.icon;
|
|
return (
|
|
<div key={idx} className="p-4 bg-slate-50 rounded-xl">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<Icon className="w-5 h-5 text-slate-400" />
|
|
<span className="font-medium text-slate-700">{kpi.label}</span>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<div className="flex justify-between items-center">
|
|
<span className="text-sm text-slate-500">Current</span>
|
|
<span className="font-semibold text-slate-900">{kpi.current}</span>
|
|
</div>
|
|
<div className="flex justify-between items-center">
|
|
<span className="text-sm text-slate-500">Target</span>
|
|
<span className="font-semibold text-green-600">{kpi.optimized}</span>
|
|
</div>
|
|
<div className="pt-2 border-t border-slate-200">
|
|
<div className="flex justify-between items-center">
|
|
<span className="text-sm text-slate-500">Impact</span>
|
|
<Badge className={kpi.trend === "up" ? "bg-green-100 text-green-700" : "bg-blue-100 text-blue-700"}>
|
|
{kpi.trend === "up" ? <TrendingUp className="w-3 h-3 mr-1" /> : <TrendingDown className="w-3 h-3 mr-1" />}
|
|
{kpi.savings}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Action Items */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Zap className="w-5 h-5 text-amber-500" />
|
|
Priority Actions for You
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-3">
|
|
{content.actions.map((item, idx) => (
|
|
<div
|
|
key={idx}
|
|
className={`p-4 rounded-xl border-2 flex items-center justify-between ${
|
|
item.priority === "high"
|
|
? "bg-red-50 border-red-200"
|
|
: "bg-amber-50 border-amber-200"
|
|
}`}
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<div className={`w-2 h-2 rounded-full ${item.priority === "high" ? "bg-red-500" : "bg-amber-500"}`} />
|
|
<div>
|
|
<p className="font-medium text-slate-900">{item.action}</p>
|
|
<div className="flex items-center gap-3 mt-1 text-sm text-slate-500">
|
|
<span className="flex items-center gap-1">
|
|
<TrendingUp className="w-3 h-3" />
|
|
{item.impact}
|
|
</span>
|
|
<span className="flex items-center gap-1">
|
|
<Clock className="w-3 h-3" />
|
|
{item.deadline}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<Badge className={item.priority === "high" ? "bg-red-100 text-red-700" : "bg-amber-100 text-amber-700"}>
|
|
{item.priority.toUpperCase()}
|
|
</Badge>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
} |