359 lines
14 KiB
JavaScript
359 lines
14 KiB
JavaScript
import React from "react";
|
|
import { Card, CardContent } from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import {
|
|
DollarSign, TrendingUp, Target, Users, Zap,
|
|
ArrowUpRight, CheckCircle, AlertTriangle, Shield,
|
|
Clock, Award, Calendar, Package, Star, BarChart3
|
|
} from "lucide-react";
|
|
|
|
export default function SavingsOverviewCards({ metrics, projections, timeRange, userRole }) {
|
|
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;
|
|
}
|
|
};
|
|
|
|
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";
|
|
}
|
|
};
|
|
|
|
// Role-specific card configurations
|
|
const getRoleCards = () => {
|
|
switch (userRole) {
|
|
case "procurement":
|
|
return [
|
|
{
|
|
title: "Vendor Performance Score",
|
|
value: `${(metrics.avgReliability + 5).toFixed(0)}%`,
|
|
change: `Top ${metrics.activeVendors} vendors tracked`,
|
|
trend: "up",
|
|
icon: Award,
|
|
color: "blue",
|
|
description: "Network-wide average",
|
|
},
|
|
{
|
|
title: "Contract Compliance",
|
|
value: `${metrics.contractedRatio.toFixed(1)}%`,
|
|
change: `${(100 - metrics.contractedRatio).toFixed(1)}% non-compliant`,
|
|
trend: metrics.contractedRatio > 70 ? "up" : "down",
|
|
icon: Shield,
|
|
color: metrics.contractedRatio > 70 ? "green" : "red",
|
|
description: "Spend under contract",
|
|
},
|
|
{
|
|
title: "Rate Optimization",
|
|
value: `$${(metrics.avgNonContractedRate - metrics.avgContractedRate).toFixed(2)}`,
|
|
change: "per hour savings potential",
|
|
trend: "up",
|
|
icon: DollarSign,
|
|
color: "emerald",
|
|
description: "Contract vs. spot rates",
|
|
},
|
|
{
|
|
title: "SLA Adherence",
|
|
value: `${metrics.fillRate.toFixed(1)}%`,
|
|
change: `${metrics.noShowRate.toFixed(1)}% no-show rate`,
|
|
trend: metrics.fillRate > 90 ? "up" : "down",
|
|
icon: CheckCircle,
|
|
color: metrics.fillRate > 90 ? "green" : "amber",
|
|
description: "Vendor delivery rate",
|
|
},
|
|
{
|
|
title: "Network Savings",
|
|
value: `$${getProjectedSavings().toLocaleString(undefined, { maximumFractionDigits: 0 })}`,
|
|
change: `${getTimeLabel()} projection`,
|
|
trend: "up",
|
|
icon: TrendingUp,
|
|
color: "purple",
|
|
description: "From vendor optimization",
|
|
},
|
|
];
|
|
|
|
case "operator":
|
|
return [
|
|
{
|
|
title: "Enterprise Fill Rate",
|
|
value: `${metrics.fillRate.toFixed(1)}%`,
|
|
change: `${metrics.completedOrders} orders fulfilled`,
|
|
trend: metrics.fillRate > 90 ? "up" : "down",
|
|
icon: Target,
|
|
color: metrics.fillRate > 90 ? "green" : "amber",
|
|
description: "Cross-sector average",
|
|
},
|
|
{
|
|
title: "Labor Efficiency",
|
|
value: `${metrics.avgReliability.toFixed(0)}%`,
|
|
change: `${metrics.noShowRate.toFixed(1)}% absence rate`,
|
|
trend: metrics.avgReliability > 85 ? "up" : "down",
|
|
icon: Users,
|
|
color: "blue",
|
|
description: "Workforce productivity",
|
|
},
|
|
{
|
|
title: "Cost per Order",
|
|
value: `$${(metrics.totalSpend / Math.max(metrics.completedOrders, 1)).toFixed(0)}`,
|
|
change: "average fulfillment cost",
|
|
trend: "up",
|
|
icon: DollarSign,
|
|
color: "purple",
|
|
description: "Operational efficiency",
|
|
},
|
|
{
|
|
title: "Sector Coverage",
|
|
value: `${metrics.activeVendors}`,
|
|
change: "active vendor partners",
|
|
trend: "up",
|
|
icon: Package,
|
|
color: "indigo",
|
|
description: "Available resources",
|
|
},
|
|
{
|
|
title: "Operational Savings",
|
|
value: `$${getProjectedSavings().toLocaleString(undefined, { maximumFractionDigits: 0 })}`,
|
|
change: `${getTimeLabel()} potential`,
|
|
trend: "up",
|
|
icon: TrendingUp,
|
|
color: "emerald",
|
|
description: "From efficiency gains",
|
|
},
|
|
];
|
|
|
|
case "sector":
|
|
return [
|
|
{
|
|
title: "Location Fill Rate",
|
|
value: `${metrics.fillRate.toFixed(1)}%`,
|
|
change: `${100 - metrics.fillRate > 0 ? (100 - metrics.fillRate).toFixed(1) + '% gaps' : 'No gaps'}`,
|
|
trend: metrics.fillRate > 90 ? "up" : "down",
|
|
icon: Target,
|
|
color: metrics.fillRate > 90 ? "green" : "red",
|
|
description: "Position coverage",
|
|
},
|
|
{
|
|
title: "Staff Reliability",
|
|
value: `${metrics.avgReliability.toFixed(0)}%`,
|
|
change: `${metrics.noShowRate.toFixed(1)}% no-shows`,
|
|
trend: metrics.avgReliability > 85 ? "up" : "down",
|
|
icon: Users,
|
|
color: metrics.avgReliability > 85 ? "blue" : "amber",
|
|
description: "At your location",
|
|
},
|
|
{
|
|
title: "Weekly Hours",
|
|
value: `${Math.floor(metrics.totalWorkforce * 32)}`,
|
|
change: "scheduled this period",
|
|
trend: "up",
|
|
icon: Clock,
|
|
color: "purple",
|
|
description: "Labor hours planned",
|
|
},
|
|
{
|
|
title: "Local Spend",
|
|
value: `$${metrics.totalSpend.toLocaleString(undefined, { maximumFractionDigits: 0 })}`,
|
|
change: "labor investment",
|
|
trend: "up",
|
|
icon: DollarSign,
|
|
color: "slate",
|
|
description: "Your location budget",
|
|
},
|
|
];
|
|
|
|
case "client":
|
|
return [
|
|
{
|
|
title: "Event Coverage",
|
|
value: `${metrics.fillRate.toFixed(1)}%`,
|
|
change: `${metrics.completedOrders} events staffed`,
|
|
trend: metrics.fillRate > 90 ? "up" : "down",
|
|
icon: Calendar,
|
|
color: metrics.fillRate > 90 ? "green" : "red",
|
|
description: "Position fill rate",
|
|
},
|
|
{
|
|
title: "Staff Quality",
|
|
value: `${metrics.avgReliability.toFixed(0)}%`,
|
|
change: "reliability score",
|
|
trend: metrics.avgReliability > 85 ? "up" : "down",
|
|
icon: Star,
|
|
color: metrics.avgReliability > 85 ? "amber" : "orange",
|
|
description: "Assigned workforce",
|
|
},
|
|
{
|
|
title: "Cost Savings",
|
|
value: `$${getProjectedSavings().toLocaleString(undefined, { maximumFractionDigits: 0 })}`,
|
|
change: `${metrics.potentialSavingsPercent.toFixed(0)}% vs. spot rates`,
|
|
trend: "up",
|
|
icon: DollarSign,
|
|
color: "emerald",
|
|
description: `${getTimeLabel()} savings`,
|
|
},
|
|
{
|
|
title: "On-Time Rate",
|
|
value: `${(100 - metrics.noShowRate).toFixed(1)}%`,
|
|
change: "staff attendance",
|
|
trend: metrics.noShowRate < 5 ? "up" : "down",
|
|
icon: CheckCircle,
|
|
color: metrics.noShowRate < 5 ? "green" : "amber",
|
|
description: "Punctuality score",
|
|
},
|
|
];
|
|
|
|
case "vendor":
|
|
return [
|
|
{
|
|
title: "Your Fill Rate",
|
|
value: `${metrics.fillRate.toFixed(1)}%`,
|
|
change: `${metrics.completedOrders} orders completed`,
|
|
trend: metrics.fillRate > 90 ? "up" : "down",
|
|
icon: Target,
|
|
color: metrics.fillRate > 90 ? "green" : "amber",
|
|
description: "Order fulfillment",
|
|
},
|
|
{
|
|
title: "Workforce Reliability",
|
|
value: `${metrics.avgReliability.toFixed(0)}%`,
|
|
change: `${metrics.noShowRate.toFixed(1)}% no-show rate`,
|
|
trend: metrics.avgReliability > 85 ? "up" : "down",
|
|
icon: Users,
|
|
color: metrics.avgReliability > 85 ? "blue" : "orange",
|
|
description: "Your team score",
|
|
},
|
|
{
|
|
title: "Competitive Edge",
|
|
value: `$${(metrics.avgNonContractedRate - metrics.avgContractedRate).toFixed(2)}/hr`,
|
|
change: "savings vs. gig rates",
|
|
trend: "up",
|
|
icon: Zap,
|
|
color: "amber",
|
|
description: "Your value proposition",
|
|
},
|
|
{
|
|
title: "Revenue Potential",
|
|
value: `$${(metrics.totalSpend * 1.2).toLocaleString(undefined, { maximumFractionDigits: 0 })}`,
|
|
change: "if 100% fill rate",
|
|
trend: "up",
|
|
icon: TrendingUp,
|
|
color: "purple",
|
|
description: "Growth opportunity",
|
|
},
|
|
{
|
|
title: "Active Workforce",
|
|
value: metrics.totalWorkforce.toString(),
|
|
change: "ready to deploy",
|
|
trend: "up",
|
|
icon: Shield,
|
|
color: "indigo",
|
|
description: "Available staff",
|
|
},
|
|
];
|
|
|
|
default: // admin
|
|
return [
|
|
{
|
|
title: `${getTimeLabel()} Potential Savings`,
|
|
value: `$${getProjectedSavings().toLocaleString(undefined, { maximumFractionDigits: 0 })}`,
|
|
change: `${metrics.potentialSavingsPercent.toFixed(1)}% opportunity`,
|
|
trend: "up",
|
|
icon: DollarSign,
|
|
color: "emerald",
|
|
description: "From contract conversion",
|
|
},
|
|
{
|
|
title: "Contract Coverage",
|
|
value: `${metrics.contractedRatio.toFixed(1)}%`,
|
|
change: `${(100 - metrics.contractedRatio).toFixed(1)}% non-contracted`,
|
|
trend: metrics.contractedRatio > 70 ? "up" : "down",
|
|
icon: Target,
|
|
color: metrics.contractedRatio > 70 ? "blue" : "amber",
|
|
description: "Labor under contract",
|
|
},
|
|
{
|
|
title: "Platform Reliability",
|
|
value: `${metrics.avgReliability.toFixed(0)}%`,
|
|
change: `${metrics.noShowRate.toFixed(1)}% no-show rate`,
|
|
trend: metrics.avgReliability > 85 ? "up" : "down",
|
|
icon: Users,
|
|
color: metrics.avgReliability > 85 ? "purple" : "orange",
|
|
description: "Workforce average",
|
|
},
|
|
{
|
|
title: "Fill Rate",
|
|
value: `${metrics.fillRate.toFixed(1)}%`,
|
|
change: `${metrics.completedOrders} orders completed`,
|
|
trend: metrics.fillRate > 90 ? "up" : "down",
|
|
icon: CheckCircle,
|
|
color: metrics.fillRate > 90 ? "green" : "red",
|
|
description: "Order fulfillment",
|
|
},
|
|
{
|
|
title: "Network Size",
|
|
value: `${metrics.activeVendors} / ${metrics.totalWorkforce}`,
|
|
change: "vendors / workforce",
|
|
trend: "up",
|
|
icon: BarChart3,
|
|
color: "indigo",
|
|
description: "Platform capacity",
|
|
},
|
|
];
|
|
}
|
|
};
|
|
|
|
const cards = getRoleCards();
|
|
|
|
const colorClasses = {
|
|
emerald: { bg: "bg-emerald-50", icon: "bg-emerald-500", text: "text-emerald-700", badge: "bg-emerald-100 text-emerald-700" },
|
|
blue: { bg: "bg-blue-50", icon: "bg-blue-500", text: "text-blue-700", badge: "bg-blue-100 text-blue-700" },
|
|
purple: { bg: "bg-purple-50", icon: "bg-purple-500", text: "text-purple-700", badge: "bg-purple-100 text-purple-700" },
|
|
green: { bg: "bg-green-50", icon: "bg-green-500", text: "text-green-700", badge: "bg-green-100 text-green-700" },
|
|
amber: { bg: "bg-amber-50", icon: "bg-amber-500", text: "text-amber-700", badge: "bg-amber-100 text-amber-700" },
|
|
orange: { bg: "bg-orange-50", icon: "bg-orange-500", text: "text-orange-700", badge: "bg-orange-100 text-orange-700" },
|
|
red: { bg: "bg-red-50", icon: "bg-red-500", text: "text-red-700", badge: "bg-red-100 text-red-700" },
|
|
indigo: { bg: "bg-indigo-50", icon: "bg-indigo-500", text: "text-indigo-700", badge: "bg-indigo-100 text-indigo-700" },
|
|
slate: { bg: "bg-slate-50", icon: "bg-slate-500", text: "text-slate-700", badge: "bg-slate-100 text-slate-700" },
|
|
};
|
|
|
|
return (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-5 gap-4">
|
|
{cards.map((card, index) => {
|
|
const colors = colorClasses[card.color];
|
|
const Icon = card.icon;
|
|
|
|
return (
|
|
<Card key={index} className={`${colors.bg} border-0 shadow-sm hover:shadow-md transition-all`}>
|
|
<CardContent className="p-5">
|
|
<div className="flex items-start justify-between mb-3">
|
|
<div className={`w-12 h-12 ${colors.icon} rounded-xl flex items-center justify-center`}>
|
|
<Icon className="w-6 h-6 text-white" />
|
|
</div>
|
|
<Badge className={`${colors.badge} border-0 text-xs font-medium`}>
|
|
{card.trend === "up" ? (
|
|
<ArrowUpRight className="w-3 h-3 mr-1" />
|
|
) : (
|
|
<AlertTriangle className="w-3 h-3 mr-1" />
|
|
)}
|
|
{card.change}
|
|
</Badge>
|
|
</div>
|
|
<p className={`text-xs ${colors.text} uppercase tracking-wider font-semibold mb-1`}>
|
|
{card.title}
|
|
</p>
|
|
<p className={`text-3xl font-bold ${colors.text}`}>{card.value}</p>
|
|
<p className="text-xs text-slate-500 mt-1">{card.description}</p>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
} |