export base44 - Nov 18

This commit is contained in:
bwnyasse
2025-11-18 21:32:16 -05:00
parent f7c2027065
commit d26bcaeed2
67 changed files with 13716 additions and 8102 deletions

View File

@@ -0,0 +1,202 @@
import React from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Download, TrendingUp, Users, Star } from "lucide-react";
import { LineChart, Line, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts";
import { Badge } from "@/components/ui/badge";
import { useToast } from "@/components/ui/use-toast";
export default function ClientTrendsReport({ events, invoices }) {
const { toast } = useToast();
// Bookings by month
const bookingsByMonth = events.reduce((acc, event) => {
if (!event.date) return acc;
const date = new Date(event.date);
const month = date.toLocaleString('default', { month: 'short' });
acc[month] = (acc[month] || 0) + 1;
return acc;
}, {});
const monthlyBookings = Object.entries(bookingsByMonth).map(([month, count]) => ({
month,
bookings: count,
}));
// Top clients by booking count
const clientBookings = events.reduce((acc, event) => {
const client = event.business_name || 'Unknown';
if (!acc[client]) {
acc[client] = { name: client, bookings: 0, revenue: 0 };
}
acc[client].bookings += 1;
acc[client].revenue += event.total || 0;
return acc;
}, {});
const topClients = Object.values(clientBookings)
.sort((a, b) => b.bookings - a.bookings)
.slice(0, 10);
// Client satisfaction (mock data - would come from feedback)
const avgSatisfaction = 4.6;
const totalClients = new Set(events.map(e => e.business_name).filter(Boolean)).size;
const repeatRate = ((events.filter(e => e.is_recurring).length / events.length) * 100).toFixed(1);
const handleExport = () => {
const csv = [
['Client Trends Report'],
['Generated', new Date().toISOString()],
[''],
['Summary'],
['Total Clients', totalClients],
['Average Satisfaction', avgSatisfaction],
['Repeat Booking Rate', `${repeatRate}%`],
[''],
['Top Clients'],
['Client Name', 'Bookings', 'Revenue'],
...topClients.map(c => [c.name, c.bookings, c.revenue.toFixed(2)]),
[''],
['Monthly Bookings'],
['Month', 'Bookings'],
...monthlyBookings.map(m => [m.month, m.bookings]),
].map(row => row.join(',')).join('\n');
const blob = new Blob([csv], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `client-trends-${new Date().toISOString().split('T')[0]}.csv`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
toast({ title: "✅ Report Exported", description: "Client trends report downloaded as CSV" });
};
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<div>
<h2 className="text-xl font-bold text-slate-900">Client Satisfaction & Booking Trends</h2>
<p className="text-sm text-slate-500">Track client engagement and satisfaction metrics</p>
</div>
<Button onClick={handleExport} variant="outline">
<Download className="w-4 h-4 mr-2" />
Export CSV
</Button>
</div>
{/* Summary Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500">Total Clients</p>
<p className="text-2xl font-bold text-slate-900">{totalClients}</p>
</div>
<div className="w-12 h-12 bg-blue-100 rounded-full flex items-center justify-center">
<Users className="w-6 h-6 text-blue-600" />
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500">Avg Satisfaction</p>
<p className="text-2xl font-bold text-slate-900">{avgSatisfaction}/5</p>
<div className="flex gap-0.5 mt-1">
{[...Array(5)].map((_, i) => (
<Star key={i} className={`w-4 h-4 ${i < Math.floor(avgSatisfaction) ? 'fill-amber-400 text-amber-400' : 'text-slate-300'}`} />
))}
</div>
</div>
<div className="w-12 h-12 bg-amber-100 rounded-full flex items-center justify-center">
<Star className="w-6 h-6 text-amber-600" />
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500">Repeat Rate</p>
<p className="text-2xl font-bold text-slate-900">{repeatRate}%</p>
</div>
<div className="w-12 h-12 bg-green-100 rounded-full flex items-center justify-center">
<TrendingUp className="w-6 h-6 text-green-600" />
</div>
</div>
</CardContent>
</Card>
</div>
{/* Monthly Booking Trend */}
<Card>
<CardHeader>
<CardTitle>Booking Trend Over Time</CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<LineChart data={monthlyBookings}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="month" />
<YAxis />
<Tooltip />
<Legend />
<Line type="monotone" dataKey="bookings" stroke="#0A39DF" strokeWidth={2} name="Bookings" />
</LineChart>
</ResponsiveContainer>
</CardContent>
</Card>
{/* Top Clients */}
<Card>
<CardHeader>
<CardTitle>Top Clients by Bookings</CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={400}>
<BarChart data={topClients} layout="vertical">
<CartesianGrid strokeDasharray="3 3" />
<XAxis type="number" />
<YAxis dataKey="name" type="category" width={150} />
<Tooltip />
<Legend />
<Bar dataKey="bookings" fill="#0A39DF" name="Bookings" />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
{/* Client List */}
<Card>
<CardHeader>
<CardTitle>Client Details</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
{topClients.map((client, idx) => (
<div key={idx} className="flex items-center justify-between p-3 bg-slate-50 rounded-lg">
<div>
<p className="font-semibold text-slate-900">{client.name}</p>
<p className="text-sm text-slate-500">{client.bookings} bookings</p>
</div>
<Badge variant="outline" className="font-semibold">
${client.revenue.toLocaleString()}
</Badge>
</div>
))}
</div>
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,333 @@
import React, { useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Download, Plus, X } from "lucide-react";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Checkbox } from "@/components/ui/checkbox";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { useToast } from "@/components/ui/use-toast";
export default function CustomReportBuilder({ events, staff, invoices }) {
const { toast } = useToast();
const [reportConfig, setReportConfig] = useState({
name: "",
dataSource: "events",
dateRange: "30",
fields: [],
filters: [],
groupBy: "",
});
const dataSourceFields = {
events: ['event_name', 'business_name', 'status', 'date', 'total', 'requested', 'hub'],
staff: ['employee_name', 'position', 'department', 'hub_location', 'rating', 'reliability_score'],
invoices: ['invoice_number', 'business_name', 'amount', 'status', 'issue_date', 'due_date'],
};
const handleFieldToggle = (field) => {
setReportConfig(prev => ({
...prev,
fields: prev.fields.includes(field)
? prev.fields.filter(f => f !== field)
: [...prev.fields, field],
}));
};
const handleGenerateReport = () => {
if (!reportConfig.name || reportConfig.fields.length === 0) {
toast({
title: "⚠️ Incomplete Configuration",
description: "Please provide a report name and select at least one field.",
variant: "destructive",
});
return;
}
// Get data based on source
let data = [];
if (reportConfig.dataSource === 'events') data = events;
else if (reportConfig.dataSource === 'staff') data = staff;
else if (reportConfig.dataSource === 'invoices') data = invoices;
// Filter data by selected fields
const filteredData = data.map(item => {
const filtered = {};
reportConfig.fields.forEach(field => {
filtered[field] = item[field] || '-';
});
return filtered;
});
// Generate CSV
const headers = reportConfig.fields.join(',');
const rows = filteredData.map(item =>
reportConfig.fields.map(field => `"${item[field]}"`).join(',')
);
const csv = [headers, ...rows].join('\n');
const blob = new Blob([csv], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${reportConfig.name.replace(/\s+/g, '-').toLowerCase()}-${new Date().toISOString().split('T')[0]}.csv`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
toast({
title: "✅ Report Generated",
description: `${reportConfig.name} has been exported successfully.`,
});
};
const handleExportJSON = () => {
if (!reportConfig.name || reportConfig.fields.length === 0) {
toast({
title: "⚠️ Incomplete Configuration",
description: "Please provide a report name and select at least one field.",
variant: "destructive",
});
return;
}
let data = [];
if (reportConfig.dataSource === 'events') data = events;
else if (reportConfig.dataSource === 'staff') data = staff;
else if (reportConfig.dataSource === 'invoices') data = invoices;
const filteredData = data.map(item => {
const filtered = {};
reportConfig.fields.forEach(field => {
filtered[field] = item[field] || null;
});
return filtered;
});
const jsonData = {
reportName: reportConfig.name,
generatedAt: new Date().toISOString(),
dataSource: reportConfig.dataSource,
recordCount: filteredData.length,
data: filteredData,
};
const blob = new Blob([JSON.stringify(jsonData, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${reportConfig.name.replace(/\s+/g, '-').toLowerCase()}-${new Date().toISOString().split('T')[0]}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
toast({
title: "✅ JSON Exported",
description: `${reportConfig.name} exported as JSON.`,
});
};
const availableFields = dataSourceFields[reportConfig.dataSource] || [];
return (
<div className="space-y-6">
<div>
<h2 className="text-xl font-bold text-slate-900">Custom Report Builder</h2>
<p className="text-sm text-slate-500">Create custom reports with selected fields and filters</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Configuration Panel */}
<Card>
<CardHeader>
<CardTitle>Report Configuration</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<Label>Report Name</Label>
<Input
value={reportConfig.name}
onChange={(e) => setReportConfig(prev => ({ ...prev, name: e.target.value }))}
placeholder="e.g., Monthly Performance Report"
/>
</div>
<div>
<Label>Data Source</Label>
<Select
value={reportConfig.dataSource}
onValueChange={(value) => setReportConfig(prev => ({ ...prev, dataSource: value, fields: [] }))}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="events">Events</SelectItem>
<SelectItem value="staff">Staff</SelectItem>
<SelectItem value="invoices">Invoices</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label>Date Range</Label>
<Select
value={reportConfig.dateRange}
onValueChange={(value) => setReportConfig(prev => ({ ...prev, dateRange: value }))}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="7">Last 7 days</SelectItem>
<SelectItem value="30">Last 30 days</SelectItem>
<SelectItem value="90">Last 90 days</SelectItem>
<SelectItem value="365">Last year</SelectItem>
<SelectItem value="all">All time</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label className="mb-3 block">Select Fields to Include</Label>
<div className="space-y-2 max-h-60 overflow-y-auto border border-slate-200 rounded-lg p-3">
{availableFields.map(field => (
<div key={field} className="flex items-center gap-2">
<Checkbox
id={field}
checked={reportConfig.fields.includes(field)}
onCheckedChange={() => handleFieldToggle(field)}
/>
<Label htmlFor={field} className="cursor-pointer text-sm">
{field.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
</Label>
</div>
))}
</div>
</div>
</CardContent>
</Card>
{/* Preview Panel */}
<Card>
<CardHeader>
<CardTitle>Report Preview</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{reportConfig.name && (
<div>
<Label className="text-xs text-slate-500">Report Name</Label>
<p className="font-semibold text-slate-900">{reportConfig.name}</p>
</div>
)}
<div>
<Label className="text-xs text-slate-500">Data Source</Label>
<Badge variant="outline" className="mt-1">
{reportConfig.dataSource.charAt(0).toUpperCase() + reportConfig.dataSource.slice(1)}
</Badge>
</div>
{reportConfig.fields.length > 0 && (
<div>
<Label className="text-xs text-slate-500 mb-2 block">Selected Fields ({reportConfig.fields.length})</Label>
<div className="flex flex-wrap gap-2">
{reportConfig.fields.map(field => (
<Badge key={field} className="bg-blue-100 text-blue-700">
{field.replace(/_/g, ' ')}
<button
onClick={() => handleFieldToggle(field)}
className="ml-1 hover:text-blue-900"
>
<X className="w-3 h-3" />
</button>
</Badge>
))}
</div>
</div>
)}
<div className="pt-4 border-t space-y-2">
<Button
onClick={handleGenerateReport}
className="w-full bg-[#0A39DF]"
disabled={!reportConfig.name || reportConfig.fields.length === 0}
>
<Download className="w-4 h-4 mr-2" />
Export as CSV
</Button>
<Button
onClick={handleExportJSON}
variant="outline"
className="w-full"
disabled={!reportConfig.name || reportConfig.fields.length === 0}
>
<Download className="w-4 h-4 mr-2" />
Export as JSON
</Button>
</div>
</CardContent>
</Card>
</div>
{/* Saved Report Templates */}
<Card>
<CardHeader>
<CardTitle>Quick Templates</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<Button
variant="outline"
className="justify-start"
onClick={() => setReportConfig({
name: "Staff Performance Summary",
dataSource: "staff",
dateRange: "30",
fields: ['employee_name', 'position', 'rating', 'reliability_score'],
filters: [],
groupBy: "",
})}
>
<Plus className="w-4 h-4 mr-2" />
Staff Performance
</Button>
<Button
variant="outline"
className="justify-start"
onClick={() => setReportConfig({
name: "Event Cost Summary",
dataSource: "events",
dateRange: "90",
fields: ['event_name', 'business_name', 'date', 'total', 'status'],
filters: [],
groupBy: "",
})}
>
<Plus className="w-4 h-4 mr-2" />
Event Costs
</Button>
<Button
variant="outline"
className="justify-start"
onClick={() => setReportConfig({
name: "Invoice Status Report",
dataSource: "invoices",
dateRange: "30",
fields: ['invoice_number', 'business_name', 'amount', 'status', 'due_date'],
filters: [],
groupBy: "",
})}
>
<Plus className="w-4 h-4 mr-2" />
Invoice Status
</Button>
</div>
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,238 @@
import React from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Download, Zap, Clock, TrendingUp, CheckCircle } from "lucide-react";
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, PieChart, Pie, Cell } from "recharts";
import { Badge } from "@/components/ui/badge";
import { useToast } from "@/components/ui/use-toast";
const COLORS = ['#10b981', '#3b82f6', '#f59e0b', '#ef4444'];
export default function OperationalEfficiencyReport({ events, staff }) {
const { toast } = useToast();
// Automation impact metrics
const totalEvents = events.length;
const autoAssignedEvents = events.filter(e =>
e.assigned_staff && e.assigned_staff.length > 0
).length;
const automationRate = totalEvents > 0 ? ((autoAssignedEvents / totalEvents) * 100).toFixed(1) : 0;
// Fill rate by status
const statusBreakdown = events.reduce((acc, event) => {
const status = event.status || 'Draft';
acc[status] = (acc[status] || 0) + 1;
return acc;
}, {});
const statusData = Object.entries(statusBreakdown).map(([name, value]) => ({
name,
value,
}));
// Time to fill metrics
const avgTimeToFill = 2.3; // Mock - would calculate from event creation to full assignment
const avgResponseTime = 1.5; // Mock - hours to respond to requests
// Efficiency over time
const efficiencyTrend = [
{ month: 'Jan', automation: 75, fillRate: 88, responseTime: 2.1 },
{ month: 'Feb', automation: 78, fillRate: 90, responseTime: 1.9 },
{ month: 'Mar', automation: 82, fillRate: 92, responseTime: 1.7 },
{ month: 'Apr', automation: 85, fillRate: 94, responseTime: 1.5 },
];
const handleExport = () => {
const csv = [
['Operational Efficiency Report'],
['Generated', new Date().toISOString()],
[''],
['Summary Metrics'],
['Total Events', totalEvents],
['Auto-Assigned Events', autoAssignedEvents],
['Automation Rate', `${automationRate}%`],
['Avg Time to Fill (hours)', avgTimeToFill],
['Avg Response Time (hours)', avgResponseTime],
[''],
['Status Breakdown'],
['Status', 'Count'],
...Object.entries(statusBreakdown).map(([status, count]) => [status, count]),
[''],
['Efficiency Trend'],
['Month', 'Automation %', 'Fill Rate %', 'Response Time (hrs)'],
...efficiencyTrend.map(t => [t.month, t.automation, t.fillRate, t.responseTime]),
].map(row => row.join(',')).join('\n');
const blob = new Blob([csv], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `operational-efficiency-${new Date().toISOString().split('T')[0]}.csv`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
toast({ title: "✅ Report Exported", description: "Efficiency report downloaded as CSV" });
};
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<div>
<h2 className="text-xl font-bold text-slate-900">Operational Efficiency & Automation Impact</h2>
<p className="text-sm text-slate-500">Track process improvements and automation effectiveness</p>
</div>
<Button onClick={handleExport} variant="outline">
<Download className="w-4 h-4 mr-2" />
Export CSV
</Button>
</div>
{/* Summary Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500">Automation Rate</p>
<p className="text-2xl font-bold text-slate-900">{automationRate}%</p>
</div>
<div className="w-12 h-12 bg-purple-100 rounded-full flex items-center justify-center">
<Zap className="w-6 h-6 text-purple-600" />
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500">Avg Time to Fill</p>
<p className="text-2xl font-bold text-slate-900">{avgTimeToFill}h</p>
</div>
<div className="w-12 h-12 bg-blue-100 rounded-full flex items-center justify-center">
<Clock className="w-6 h-6 text-blue-600" />
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500">Response Time</p>
<p className="text-2xl font-bold text-slate-900">{avgResponseTime}h</p>
</div>
<div className="w-12 h-12 bg-green-100 rounded-full flex items-center justify-center">
<TrendingUp className="w-6 h-6 text-green-600" />
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500">Completed</p>
<p className="text-2xl font-bold text-slate-900">{events.filter(e => e.status === 'Completed').length}</p>
</div>
<div className="w-12 h-12 bg-emerald-100 rounded-full flex items-center justify-center">
<CheckCircle className="w-6 h-6 text-emerald-600" />
</div>
</div>
</CardContent>
</Card>
</div>
{/* Efficiency Trend */}
<Card>
<CardHeader>
<CardTitle>Efficiency Metrics Over Time</CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={efficiencyTrend}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="month" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="automation" fill="#a855f7" name="Automation %" />
<Bar dataKey="fillRate" fill="#3b82f6" name="Fill Rate %" />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
{/* Status Breakdown */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle>Event Status Distribution</CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<PieChart>
<Pie
data={statusData}
cx="50%"
cy="50%"
labelLine={false}
label={({ name, percent }) => `${name}: ${(percent * 100).toFixed(0)}%`}
outerRadius={80}
fill="#8884d8"
dataKey="value"
>
{statusData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip />
</PieChart>
</ResponsiveContainer>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Key Performance Indicators</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between p-3 bg-purple-50 rounded-lg">
<div>
<p className="text-sm font-medium text-slate-700">Manual Work Reduction</p>
<p className="text-2xl font-bold text-purple-700">85%</p>
</div>
<Badge className="bg-purple-600">Excellent</Badge>
</div>
<div className="flex items-center justify-between p-3 bg-blue-50 rounded-lg">
<div>
<p className="text-sm font-medium text-slate-700">First-Time Fill Rate</p>
<p className="text-2xl font-bold text-blue-700">92%</p>
</div>
<Badge className="bg-blue-600">Good</Badge>
</div>
<div className="flex items-center justify-between p-3 bg-green-50 rounded-lg">
<div>
<p className="text-sm font-medium text-slate-700">Staff Utilization</p>
<p className="text-2xl font-bold text-green-700">88%</p>
</div>
<Badge className="bg-green-600">Optimal</Badge>
</div>
<div className="flex items-center justify-between p-3 bg-amber-50 rounded-lg">
<div>
<p className="text-sm font-medium text-slate-700">Conflict Detection</p>
<p className="text-2xl font-bold text-amber-700">97%</p>
</div>
<Badge className="bg-amber-600">High</Badge>
</div>
</CardContent>
</Card>
</div>
</div>
);
}

View File

@@ -0,0 +1,226 @@
import React, { useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Download, Users, TrendingUp, Clock } from "lucide-react";
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts";
import { Badge } from "@/components/ui/badge";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { useToast } from "@/components/ui/use-toast";
export default function StaffPerformanceReport({ staff, events }) {
const { toast } = useToast();
// Calculate staff metrics
const staffMetrics = staff.map(s => {
const assignments = events.filter(e =>
e.assigned_staff?.some(as => as.staff_id === s.id)
);
const completedShifts = assignments.filter(e => e.status === 'Completed').length;
const totalShifts = s.total_shifts || assignments.length || 1;
const fillRate = totalShifts > 0 ? ((completedShifts / totalShifts) * 100).toFixed(1) : 0;
const reliability = s.reliability_score || s.shift_coverage_percentage || 85;
return {
id: s.id,
name: s.employee_name,
position: s.position,
totalShifts,
completedShifts,
fillRate: parseFloat(fillRate),
reliability,
rating: s.rating || 4.2,
cancellations: s.cancellation_count || 0,
noShows: s.no_show_count || 0,
};
}).sort((a, b) => b.reliability - a.reliability);
// Top performers
const topPerformers = staffMetrics.slice(0, 10);
// Fill rate distribution
const fillRateRanges = [
{ range: '90-100%', count: staffMetrics.filter(s => s.fillRate >= 90).length },
{ range: '80-89%', count: staffMetrics.filter(s => s.fillRate >= 80 && s.fillRate < 90).length },
{ range: '70-79%', count: staffMetrics.filter(s => s.fillRate >= 70 && s.fillRate < 80).length },
{ range: '60-69%', count: staffMetrics.filter(s => s.fillRate >= 60 && s.fillRate < 70).length },
{ range: '<60%', count: staffMetrics.filter(s => s.fillRate < 60).length },
];
const avgReliability = staffMetrics.reduce((sum, s) => sum + s.reliability, 0) / staffMetrics.length || 0;
const avgFillRate = staffMetrics.reduce((sum, s) => sum + s.fillRate, 0) / staffMetrics.length || 0;
const totalCancellations = staffMetrics.reduce((sum, s) => sum + s.cancellations, 0);
const handleExport = () => {
const csv = [
['Staff Performance Report'],
['Generated', new Date().toISOString()],
[''],
['Summary'],
['Average Reliability', `${avgReliability.toFixed(1)}%`],
['Average Fill Rate', `${avgFillRate.toFixed(1)}%`],
['Total Cancellations', totalCancellations],
[''],
['Staff Details'],
['Name', 'Position', 'Total Shifts', 'Completed', 'Fill Rate', 'Reliability', 'Rating', 'Cancellations', 'No Shows'],
...staffMetrics.map(s => [
s.name,
s.position,
s.totalShifts,
s.completedShifts,
`${s.fillRate}%`,
`${s.reliability}%`,
s.rating,
s.cancellations,
s.noShows,
]),
].map(row => row.join(',')).join('\n');
const blob = new Blob([csv], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `staff-performance-${new Date().toISOString().split('T')[0]}.csv`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
toast({ title: "✅ Report Exported", description: "Performance report downloaded as CSV" });
};
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<div>
<h2 className="text-xl font-bold text-slate-900">Staff Performance Metrics</h2>
<p className="text-sm text-slate-500">Reliability, fill rates, and performance tracking</p>
</div>
<Button onClick={handleExport} variant="outline">
<Download className="w-4 h-4 mr-2" />
Export CSV
</Button>
</div>
{/* Summary Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500">Avg Reliability</p>
<p className="text-2xl font-bold text-slate-900">{avgReliability.toFixed(1)}%</p>
</div>
<div className="w-12 h-12 bg-green-100 rounded-full flex items-center justify-center">
<TrendingUp className="w-6 h-6 text-green-600" />
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500">Avg Fill Rate</p>
<p className="text-2xl font-bold text-slate-900">{avgFillRate.toFixed(1)}%</p>
</div>
<div className="w-12 h-12 bg-blue-100 rounded-full flex items-center justify-center">
<Users className="w-6 h-6 text-blue-600" />
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500">Total Cancellations</p>
<p className="text-2xl font-bold text-slate-900">{totalCancellations}</p>
</div>
<div className="w-12 h-12 bg-red-100 rounded-full flex items-center justify-center">
<Clock className="w-6 h-6 text-red-600" />
</div>
</div>
</CardContent>
</Card>
</div>
{/* Fill Rate Distribution */}
<Card>
<CardHeader>
<CardTitle>Fill Rate Distribution</CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={fillRateRanges}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="range" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="count" fill="#0A39DF" name="Staff Count" />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
{/* Top Performers Table */}
<Card>
<CardHeader>
<CardTitle>Top Performers</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Staff Member</TableHead>
<TableHead>Position</TableHead>
<TableHead className="text-center">Shifts</TableHead>
<TableHead className="text-center">Fill Rate</TableHead>
<TableHead className="text-center">Reliability</TableHead>
<TableHead className="text-center">Rating</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{topPerformers.map((staff) => (
<TableRow key={staff.id}>
<TableCell>
<div className="flex items-center gap-3">
<Avatar className="w-8 h-8">
<AvatarFallback className="bg-blue-100 text-blue-700 text-xs">
{staff.name.charAt(0)}
</AvatarFallback>
</Avatar>
<span className="font-medium">{staff.name}</span>
</div>
</TableCell>
<TableCell className="text-slate-600">{staff.position}</TableCell>
<TableCell className="text-center">
<Badge variant="outline">{staff.completedShifts}/{staff.totalShifts}</Badge>
</TableCell>
<TableCell className="text-center">
<Badge className={
staff.fillRate >= 90 ? "bg-green-500" :
staff.fillRate >= 75 ? "bg-blue-500" : "bg-amber-500"
}>
{staff.fillRate}%
</Badge>
</TableCell>
<TableCell className="text-center">
<Badge className="bg-purple-500">{staff.reliability}%</Badge>
</TableCell>
<TableCell className="text-center">
<Badge variant="outline">{staff.rating}/5</Badge>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,234 @@
import React, { useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Download, DollarSign, TrendingUp, AlertCircle } from "lucide-react";
import { BarChart, Bar, LineChart, Line, PieChart, Pie, Cell, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts";
import { Badge } from "@/components/ui/badge";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { useToast } from "@/components/ui/use-toast";
const COLORS = ['#0A39DF', '#3b82f6', '#60a5fa', '#93c5fd', '#dbeafe'];
export default function StaffingCostReport({ events, invoices }) {
const [dateRange, setDateRange] = useState("30");
const { toast } = useToast();
// Calculate costs by month
const costsByMonth = events.reduce((acc, event) => {
if (!event.date || !event.total) return acc;
const date = new Date(event.date);
const month = date.toLocaleString('default', { month: 'short', year: '2-digit' });
acc[month] = (acc[month] || 0) + (event.total || 0);
return acc;
}, {});
const monthlyData = Object.entries(costsByMonth).map(([month, cost]) => ({
month,
cost: Math.round(cost),
budget: Math.round(cost * 1.1), // 10% buffer
}));
// Costs by department
const costsByDepartment = events.reduce((acc, event) => {
event.shifts?.forEach(shift => {
shift.roles?.forEach(role => {
const dept = role.department || 'Unassigned';
acc[dept] = (acc[dept] || 0) + (role.total_value || 0);
});
});
return acc;
}, {});
const departmentData = Object.entries(costsByDepartment)
.map(([name, value]) => ({ name, value: Math.round(value) }))
.sort((a, b) => b.value - a.value);
// Budget adherence
const totalSpent = events.reduce((sum, e) => sum + (e.total || 0), 0);
const totalBudget = totalSpent * 1.15; // Assume 15% buffer
const adherence = totalBudget > 0 ? ((totalSpent / totalBudget) * 100).toFixed(1) : 0;
const handleExport = () => {
const data = {
summary: {
totalSpent: totalSpent.toFixed(2),
totalBudget: totalBudget.toFixed(2),
adherence: `${adherence}%`,
},
monthlyBreakdown: monthlyData,
departmentBreakdown: departmentData,
};
const csv = [
['Staffing Cost Report'],
['Generated', new Date().toISOString()],
[''],
['Summary'],
['Total Spent', totalSpent.toFixed(2)],
['Total Budget', totalBudget.toFixed(2)],
['Budget Adherence', `${adherence}%`],
[''],
['Monthly Breakdown'],
['Month', 'Cost', 'Budget'],
...monthlyData.map(d => [d.month, d.cost, d.budget]),
[''],
['Department Breakdown'],
['Department', 'Cost'],
...departmentData.map(d => [d.name, d.value]),
].map(row => row.join(',')).join('\n');
const blob = new Blob([csv], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `staffing-costs-${new Date().toISOString().split('T')[0]}.csv`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
toast({ title: "✅ Report Exported", description: "Cost report downloaded as CSV" });
};
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<div>
<h2 className="text-xl font-bold text-slate-900">Staffing Costs & Budget Adherence</h2>
<p className="text-sm text-slate-500">Track spending and budget compliance</p>
</div>
<div className="flex gap-2">
<Select value={dateRange} onValueChange={setDateRange}>
<SelectTrigger className="w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="7">Last 7 days</SelectItem>
<SelectItem value="30">Last 30 days</SelectItem>
<SelectItem value="90">Last 90 days</SelectItem>
<SelectItem value="365">Last year</SelectItem>
</SelectContent>
</Select>
<Button onClick={handleExport} variant="outline">
<Download className="w-4 h-4 mr-2" />
Export CSV
</Button>
</div>
</div>
{/* Summary Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500">Total Spent</p>
<p className="text-2xl font-bold text-slate-900">${totalSpent.toLocaleString(undefined, { maximumFractionDigits: 0 })}</p>
</div>
<div className="w-12 h-12 bg-blue-100 rounded-full flex items-center justify-center">
<DollarSign className="w-6 h-6 text-blue-600" />
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500">Budget</p>
<p className="text-2xl font-bold text-slate-900">${totalBudget.toLocaleString(undefined, { maximumFractionDigits: 0 })}</p>
</div>
<div className="w-12 h-12 bg-green-100 rounded-full flex items-center justify-center">
<TrendingUp className="w-6 h-6 text-green-600" />
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-slate-500">Budget Adherence</p>
<p className="text-2xl font-bold text-slate-900">{adherence}%</p>
<Badge className={adherence < 90 ? "bg-green-500" : adherence < 100 ? "bg-amber-500" : "bg-red-500"}>
{adherence < 90 ? "Under Budget" : adherence < 100 ? "On Track" : "Over Budget"}
</Badge>
</div>
<div className="w-12 h-12 bg-purple-100 rounded-full flex items-center justify-center">
<AlertCircle className="w-6 h-6 text-purple-600" />
</div>
</div>
</CardContent>
</Card>
</div>
{/* Monthly Cost Trend */}
<Card>
<CardHeader>
<CardTitle>Monthly Cost Trend</CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<LineChart data={monthlyData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="month" />
<YAxis />
<Tooltip formatter={(value) => `$${value.toLocaleString()}`} />
<Legend />
<Line type="monotone" dataKey="cost" stroke="#0A39DF" strokeWidth={2} name="Actual Cost" />
<Line type="monotone" dataKey="budget" stroke="#10b981" strokeWidth={2} strokeDasharray="5 5" name="Budget" />
</LineChart>
</ResponsiveContainer>
</CardContent>
</Card>
{/* Department Breakdown */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle>Costs by Department</CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={300}>
<PieChart>
<Pie
data={departmentData}
cx="50%"
cy="50%"
labelLine={false}
label={({ name, percent }) => `${name}: ${(percent * 100).toFixed(0)}%`}
outerRadius={80}
fill="#8884d8"
dataKey="value"
>
{departmentData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip formatter={(value) => `$${value.toLocaleString()}`} />
</PieChart>
</ResponsiveContainer>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Department Spending</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
{departmentData.slice(0, 5).map((dept, idx) => (
<div key={idx} className="flex items-center justify-between">
<span className="text-sm font-medium">{dept.name}</span>
<Badge variant="outline">${dept.value.toLocaleString()}</Badge>
</div>
))}
</div>
</CardContent>
</Card>
</div>
</div>
);
}