272 lines
13 KiB
JavaScript
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>
|
|
);
|
|
} |