Files
Krow-workspace/frontend-web/src/components/savings/LaborSpendAnalysis.jsx
2025-12-26 15:14:51 -05:00

272 lines
13 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 { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import {
Users, DollarSign, Clock, TrendingUp, TrendingDown,
Building2, MapPin, Briefcase, AlertTriangle, CheckCircle
} from "lucide-react";
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from "recharts";
export default function LaborSpendAnalysis({ assignments, workforce, orders, metrics, userRole }) {
// Generate channel breakdown
const channelData = [
{ channel: "Preferred Vendors", spend: metrics.contractedSpend * 0.6, workers: Math.floor(workforce.length * 0.4), efficiency: 94 },
{ channel: "Approved Vendors", spend: metrics.contractedSpend * 0.4, workers: Math.floor(workforce.length * 0.25), efficiency: 88 },
{ channel: "Gig Platforms", spend: metrics.nonContractedSpend * 0.5, workers: Math.floor(workforce.length * 0.2), efficiency: 72 },
{ channel: "Agency Labor", spend: metrics.nonContractedSpend * 0.3, workers: Math.floor(workforce.length * 0.1), efficiency: 68 },
{ channel: "Internal Pool", spend: metrics.nonContractedSpend * 0.2, workers: Math.floor(workforce.length * 0.05), efficiency: 91 },
];
const utilizationData = [
{ category: "Culinary Staff", utilized: 85, available: 100, cost: 45000 },
{ category: "Event Staff", utilized: 78, available: 100, cost: 32000 },
{ category: "Bartenders", utilized: 92, available: 100, cost: 28000 },
{ category: "Security", utilized: 65, available: 100, cost: 18000 },
{ category: "Facilities", utilized: 71, available: 100, cost: 15000 },
];
const benchmarkData = [
{ metric: "Cost per Hour", yours: metrics.avgNonContractedRate, benchmark: 42.50, industry: 48.00 },
{ metric: "Fill Rate", yours: metrics.fillRate, benchmark: 95, industry: 88 },
{ metric: "No-Show Rate", yours: metrics.noShowRate, benchmark: 2, industry: 5 },
{ metric: "OT Percentage", yours: 12, benchmark: 8, industry: 15 },
];
return (
<div className="space-y-6">
{/* Channel Breakdown */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Building2 className="w-5 h-5 text-blue-500" />
Labor Spend by Channel
</CardTitle>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow className="bg-slate-50">
<TableHead>Channel</TableHead>
<TableHead className="text-right">Spend</TableHead>
<TableHead className="text-right">Workers</TableHead>
<TableHead className="text-right">Efficiency</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{channelData.map((row, idx) => (
<TableRow key={idx}>
<TableCell className="font-medium">{row.channel}</TableCell>
<TableCell className="text-right font-semibold">
${row.spend.toLocaleString(undefined, { maximumFractionDigits: 0 })}
</TableCell>
<TableCell className="text-right">{row.workers}</TableCell>
<TableCell className="text-right">
<div className="flex items-center justify-end gap-2">
<Progress value={row.efficiency} className="w-16 h-2" />
<span className="text-sm font-medium">{row.efficiency}%</span>
</div>
</TableCell>
<TableCell>
<Badge className={row.efficiency >= 85 ? "bg-green-100 text-green-700" : row.efficiency >= 70 ? "bg-amber-100 text-amber-700" : "bg-red-100 text-red-700"}>
{row.efficiency >= 85 ? "Optimal" : row.efficiency >= 70 ? "Review" : "Optimize"}
</Badge>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
{/* Utilization Chart */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Users className="w-5 h-5 text-purple-500" />
Workforce Utilization by Category
</CardTitle>
</CardHeader>
<CardContent>
<div className="h-72">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={utilizationData} layout="vertical">
<CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" />
<XAxis type="number" domain={[0, 100]} stroke="#64748b" fontSize={12} />
<YAxis type="category" dataKey="category" stroke="#64748b" fontSize={12} width={100} />
<Tooltip
formatter={(value) => [`${value}%`, 'Utilization']}
contentStyle={{ background: 'white', border: '1px solid #e2e8f0', borderRadius: '8px' }}
/>
<Bar dataKey="utilized" fill="#0A39DF" radius={[0, 4, 4, 0]} name="Utilized" />
</BarChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<DollarSign className="w-5 h-5 text-green-500" />
Cost by Category
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{utilizationData.map((item, idx) => (
<div key={idx} className="flex items-center justify-between p-3 bg-slate-50 rounded-lg">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-[#0A39DF] rounded-lg flex items-center justify-center">
<Users className="w-5 h-5 text-white" />
</div>
<div>
<p className="font-medium text-slate-900">{item.category}</p>
<p className="text-sm text-slate-500">{item.utilized}% utilized</p>
</div>
</div>
<div className="text-right">
<p className="font-bold text-slate-900">${item.cost.toLocaleString()}</p>
<p className="text-xs text-slate-500">monthly spend</p>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</div>
{/* Benchmarking */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<TrendingUp className="w-5 h-5 text-amber-500" />
Performance Benchmarking
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{benchmarkData.map((item, idx) => {
const isBetter = item.metric === "No-Show Rate" || item.metric === "OT Percentage"
? item.yours < item.benchmark
: item.yours > item.benchmark;
return (
<div key={idx} className="p-4 bg-slate-50 rounded-xl">
<p className="text-sm text-slate-500 mb-2">{item.metric}</p>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm text-slate-600">Your Rate</span>
<span className={`font-bold ${isBetter ? "text-green-600" : "text-red-600"}`}>
{item.metric.includes("Rate") || item.metric.includes("Percentage")
? `${item.yours.toFixed(1)}%`
: `$${item.yours.toFixed(2)}`}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-slate-600">Benchmark</span>
<span className="font-medium text-slate-900">
{item.metric.includes("Rate") || item.metric.includes("Percentage")
? `${item.benchmark}%`
: `$${item.benchmark.toFixed(2)}`}
</span>
</div>
<div className="flex items-center justify-between pt-2 border-t border-slate-200">
<span className="text-sm text-slate-600">Industry Avg</span>
<span className="text-slate-500">
{item.metric.includes("Rate") || item.metric.includes("Percentage")
? `${item.industry}%`
: `$${item.industry.toFixed(2)}`}
</span>
</div>
</div>
<div className="mt-3">
<Badge className={isBetter ? "bg-green-100 text-green-700" : "bg-red-100 text-red-700"}>
{isBetter ? <CheckCircle className="w-3 h-3 mr-1" /> : <AlertTriangle className="w-3 h-3 mr-1" />}
{isBetter ? "Above Benchmark" : "Below Benchmark"}
</Badge>
</div>
</div>
);
})}
</div>
</CardContent>
</Card>
{/* Contracted vs Non-Contracted Comparison */}
<Card>
<CardHeader>
<CardTitle>Contracted vs. Non-Contracted Labor Analysis</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="p-6 bg-green-50 rounded-xl border-2 border-green-200">
<div className="flex items-center gap-3 mb-4">
<div className="w-12 h-12 bg-green-500 rounded-xl flex items-center justify-center">
<CheckCircle className="w-6 h-6 text-white" />
</div>
<div>
<h4 className="font-bold text-green-900">Contracted Labor</h4>
<p className="text-sm text-green-700">{metrics.contractedRatio.toFixed(1)}% of total spend</p>
</div>
</div>
<div className="space-y-3">
<div className="flex justify-between">
<span className="text-green-700">Total Spend</span>
<span className="font-bold text-green-900">${metrics.contractedSpend.toLocaleString(undefined, { maximumFractionDigits: 0 })}</span>
</div>
<div className="flex justify-between">
<span className="text-green-700">Avg Rate</span>
<span className="font-bold text-green-900">${metrics.avgContractedRate.toFixed(2)}/hr</span>
</div>
<div className="flex justify-between">
<span className="text-green-700">Reliability</span>
<span className="font-bold text-green-900">92%</span>
</div>
<div className="flex justify-between">
<span className="text-green-700">Fill Rate</span>
<span className="font-bold text-green-900">96%</span>
</div>
</div>
</div>
<div className="p-6 bg-red-50 rounded-xl border-2 border-red-200">
<div className="flex items-center gap-3 mb-4">
<div className="w-12 h-12 bg-red-500 rounded-xl flex items-center justify-center">
<AlertTriangle className="w-6 h-6 text-white" />
</div>
<div>
<h4 className="font-bold text-red-900">Non-Contracted Labor</h4>
<p className="text-sm text-red-700">{(100 - metrics.contractedRatio).toFixed(1)}% of total spend</p>
</div>
</div>
<div className="space-y-3">
<div className="flex justify-between">
<span className="text-red-700">Total Spend</span>
<span className="font-bold text-red-900">${metrics.nonContractedSpend.toLocaleString(undefined, { maximumFractionDigits: 0 })}</span>
</div>
<div className="flex justify-between">
<span className="text-red-700">Avg Rate</span>
<span className="font-bold text-red-900">${metrics.avgNonContractedRate.toFixed(2)}/hr</span>
</div>
<div className="flex justify-between">
<span className="text-red-700">Reliability</span>
<span className="font-bold text-red-900">71%</span>
</div>
<div className="flex justify-between">
<span className="text-red-700">Fill Rate</span>
<span className="font-bold text-red-900">78%</span>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
);
}