This commit establishes the new monorepo architecture for the KROW Workforce platform. Key changes include: - Reorganized project into `frontend-web`, `mobile-apps`, `firebase`, `scripts`, and `secrets` directories. - Updated `Makefile` to support the new monorepo layout and automate Base44 export integration. - Fixed `scripts/prepare-export.js` for ES module compatibility and global component import resolution. - Created and updated `CONTRIBUTING.md` for developer onboarding. - Restructured, renamed, and translated all `docs/` files for clarity and consistency. - Implemented an interactive internal launchpad with diagram viewing capabilities. - Configured base Firebase project files (`firebase.json`, security rules). - Updated `README.md` to reflect the new project structure and documentation overview.
1284 lines
58 KiB
JavaScript
1284 lines
58 KiB
JavaScript
|
||
import React, { useState, useEffect } from "react";
|
||
import { base44 } from "@/api/base44Client";
|
||
import { useQuery } from "@tanstack/react-query";
|
||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||
import { Badge } from "@/components/ui/badge";
|
||
import { Button } from "@/components/ui/button";
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
DialogDescription,
|
||
} from "@/components/ui/dialog";
|
||
import {
|
||
BarChart3,
|
||
TrendingUp,
|
||
TrendingDown,
|
||
AlertTriangle,
|
||
CheckCircle2,
|
||
DollarSign,
|
||
Award,
|
||
Shield,
|
||
Target,
|
||
Coffee,
|
||
ArrowRight,
|
||
Clock,
|
||
AlertCircle,
|
||
Activity,
|
||
Users,
|
||
FileText,
|
||
ChevronRight,
|
||
Edit,
|
||
X,
|
||
Calendar,
|
||
Package,
|
||
RefreshCw,
|
||
Printer,
|
||
Mail,
|
||
Download,
|
||
User,
|
||
Menu,
|
||
HelpCircle
|
||
} from "lucide-react";
|
||
import { format } from "date-fns";
|
||
import { createPageUrl } from "@/utils";
|
||
import { Link } from "react-router-dom";
|
||
import { LineChart, Line, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, AreaChart, Area, PieChart, Pie, Cell } from 'recharts';
|
||
|
||
const MARKET_AVERAGES = {
|
||
"Banquet Captain": 47.76,
|
||
"Server": 41.56,
|
||
"Cook": 44.58,
|
||
"Line Cook": 44.58,
|
||
"Full Bartender": 47.76,
|
||
"Sous Chef": 59.75,
|
||
"Executive Chef": 70.60,
|
||
"Dishwasher/ Steward": 38.38,
|
||
"Prep Cook": 37.98,
|
||
"Barback": 36.13,
|
||
"Busser": 39.23,
|
||
};
|
||
|
||
const COLORS = ['#0A39DF', '#10b981', '#f59e0b', '#8b5cf6', '#ec4899', '#ef4444', '#3b82f6'];
|
||
|
||
// Safe date formatter
|
||
const safeFormatDate = (dateString, formatStr) => {
|
||
if (!dateString) return "N/A";
|
||
try {
|
||
const date = new Date(dateString);
|
||
if (isNaN(date.getTime())) return "N/A";
|
||
return format(date, formatStr);
|
||
} catch {
|
||
return "N/A";
|
||
}
|
||
};
|
||
|
||
export default function Reports() {
|
||
const [greeting, setGreeting] = useState("");
|
||
const [reviewVendor, setReviewVendor] = useState(null);
|
||
const [showReviewDialog, setShowReviewDialog] = useState(false);
|
||
|
||
useEffect(() => {
|
||
const hour = new Date().getHours();
|
||
if (hour < 12) {
|
||
setGreeting("Good Morning");
|
||
} else if (hour < 18) {
|
||
setGreeting("Good Afternoon");
|
||
} else {
|
||
setGreeting("Good Evening");
|
||
}
|
||
}, []);
|
||
|
||
const { data: user } = useQuery({
|
||
queryKey: ['current-user-reports'],
|
||
queryFn: () => base44.auth.me(),
|
||
});
|
||
|
||
const { data: rates = [] } = useQuery({
|
||
queryKey: ['vendor-rates-analysis'],
|
||
queryFn: () => base44.entities.VendorRate.list(),
|
||
initialData: [],
|
||
});
|
||
|
||
const { data: events = [] } = useQuery({
|
||
queryKey: ['events-analysis'],
|
||
queryFn: () => base44.entities.Event.list('-date'),
|
||
initialData: [],
|
||
});
|
||
|
||
const { data: staff = [] } = useQuery({
|
||
queryKey: ['staff-analysis'],
|
||
queryFn: () => base44.entities.Staff.list(),
|
||
initialData: [],
|
||
});
|
||
|
||
const userRole = user?.user_role || user?.role || "admin";
|
||
|
||
// Filter data based on user role
|
||
const getFilteredEvents = () => {
|
||
if (userRole === "client") {
|
||
return events.filter(e =>
|
||
e.client_email === user?.email ||
|
||
e.business_name === user?.company_name ||
|
||
e.created_by === user?.email
|
||
);
|
||
}
|
||
if (userRole === "vendor") {
|
||
return events.filter(e => e.vendor_name === user?.company_name);
|
||
}
|
||
// For admin, operator, sector (to see own shifts), return all or adjust based on specific needs
|
||
// For workforce, events might not be directly relevant unless they are assigned to them
|
||
// For now, return all for admin/operator/sector if no specific event filtering
|
||
return events;
|
||
};
|
||
|
||
const getFilteredStaff = () => {
|
||
if (userRole === "vendor") {
|
||
return staff.filter(s => s.vendor_name === user?.company_name);
|
||
}
|
||
if (userRole === "workforce") {
|
||
// Assuming employee_name matches full_name or created_by matches email
|
||
return staff.filter(s => s.employee_name === user?.full_name || s.created_by === user?.email);
|
||
}
|
||
// For admin, client (to see all staff for their events), operator, sector
|
||
return staff;
|
||
};
|
||
|
||
const filteredEvents = getFilteredEvents();
|
||
const filteredStaff = getFilteredStaff();
|
||
|
||
// Analyze vendor performance (used by admin/procurement)
|
||
const analyzeVendors = () => {
|
||
const vendorStats = {};
|
||
|
||
rates.forEach(rate => {
|
||
if (!vendorStats[rate.vendor_name]) {
|
||
vendorStats[rate.vendor_name] = {
|
||
name: rate.vendor_name,
|
||
totalServices: 0,
|
||
activeServices: 0,
|
||
avgMarkup: 0,
|
||
avgVendorFee: 0,
|
||
underpriced: 0,
|
||
overpriced: 0,
|
||
optimal: 0,
|
||
complianceIssues: 0,
|
||
markups: [],
|
||
fees: [],
|
||
underpricedServices: [],
|
||
complianceIssueServices: []
|
||
};
|
||
}
|
||
|
||
const vendor = vendorStats[rate.vendor_name];
|
||
vendor.totalServices++;
|
||
if (rate.is_active) vendor.activeServices++;
|
||
vendor.markups.push(rate.markup_percentage);
|
||
vendor.fees.push(rate.vendor_fee_percentage);
|
||
|
||
// Check minimum wage compliance
|
||
if (rate.employee_wage < 16.50) {
|
||
vendor.complianceIssues++;
|
||
vendor.complianceIssueServices.push({
|
||
...rate,
|
||
issue: 'Below minimum wage',
|
||
currentWage: rate.employee_wage,
|
||
requiredWage: 16.50,
|
||
difference: (16.50 - rate.employee_wage).toFixed(2)
|
||
});
|
||
}
|
||
|
||
// Price analysis
|
||
const marketAvg = MARKET_AVERAGES[rate.role_name];
|
||
if (marketAvg) {
|
||
const diff = ((rate.client_rate - marketAvg) / marketAvg) * 100;
|
||
if (diff < -15) {
|
||
vendor.underpriced++;
|
||
vendor.underpricedServices.push({
|
||
...rate,
|
||
issue: 'Underpriced',
|
||
marketAverage: marketAvg,
|
||
currentRate: rate.client_rate,
|
||
percentageBelow: Math.abs(diff).toFixed(1),
|
||
potentialRevenue: ((marketAvg - rate.client_rate) * 160).toFixed(0) // Assuming 160 hours/month
|
||
});
|
||
} else if (diff > 20) {
|
||
vendor.overpriced++;
|
||
} else if (diff >= -5 && diff <= 10) {
|
||
vendor.optimal++;
|
||
}
|
||
}
|
||
});
|
||
|
||
// Calculate averages
|
||
Object.values(vendorStats).forEach(vendor => {
|
||
vendor.avgMarkup = vendor.markups.length > 0
|
||
? (vendor.markups.reduce((a, b) => a + b, 0) / vendor.markups.length).toFixed(1)
|
||
: 0;
|
||
vendor.avgVendorFee = vendor.fees.length > 0
|
||
? (vendor.fees.reduce((a, b) => a + b, 0) / vendor.fees.length).toFixed(1)
|
||
: 0;
|
||
});
|
||
|
||
return Object.values(vendorStats);
|
||
};
|
||
|
||
const vendorAnalysis = analyzeVendors();
|
||
|
||
const topPerformers = vendorAnalysis
|
||
.filter(v => v.optimal > 0)
|
||
.sort((a, b) => b.optimal - a.optimal)
|
||
.slice(0, 3);
|
||
|
||
const underperformers = vendorAnalysis
|
||
.filter(v => v.complianceIssues > 0 || v.underpriced > v.optimal)
|
||
.sort((a, b) => (b.complianceIssues + b.underpriced) - (a.complianceIssues + a.underpriced))
|
||
.slice(0, 3);
|
||
|
||
const underchargingVendors = vendorAnalysis
|
||
.filter(v => v.underpriced > 2)
|
||
.sort((a, b) => b.underpriced - a.underpriced)
|
||
.slice(0, 3);
|
||
|
||
const complianceIssues = vendorAnalysis
|
||
.filter(v => v.complianceIssues > 0)
|
||
.sort((a, b) => b.complianceIssues - a.complianceIssues);
|
||
|
||
const totalVendors = vendorAnalysis.length;
|
||
const totalServices = rates.length;
|
||
const activeServices = rates.filter(r => r.is_active).length;
|
||
const totalCompliance = rates.filter(r => r.employee_wage >= 16.50).length;
|
||
const complianceRate = totalServices > 0 ? ((totalCompliance / totalServices) * 100).toFixed(1) : 0;
|
||
const revenueOpportunities = underchargingVendors.reduce((sum, v) => sum + (v.underpriced * 5000), 0); // Placeholder revenue opportunity
|
||
|
||
const handleReviewVendor = (vendor) => {
|
||
setReviewVendor(vendor);
|
||
setShowReviewDialog(true);
|
||
};
|
||
|
||
const renderAdminProcurementReports = () => {
|
||
return (
|
||
<>
|
||
{/* Key Metrics */}
|
||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||
<Card className="border-slate-200">
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<div className="w-10 h-10 bg-emerald-50 rounded-lg flex items-center justify-center">
|
||
<CheckCircle2 className="w-5 h-5 text-emerald-600" />
|
||
</div>
|
||
<Badge variant="outline" className="text-emerald-700 border-emerald-200 bg-emerald-50">
|
||
Active
|
||
</Badge>
|
||
</div>
|
||
<p className="text-3xl font-bold text-slate-900 mb-1">{totalVendors}</p>
|
||
<p className="text-sm text-slate-600">Active Vendors</p>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card className="border-slate-200">
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<div className="w-10 h-10 bg-blue-50 rounded-lg flex items-center justify-center">
|
||
<Target className="w-5 h-5 text-blue-600" />
|
||
</div>
|
||
<Badge variant="outline" className="text-blue-700 border-blue-200 bg-blue-50">
|
||
Services
|
||
</Badge>
|
||
</div>
|
||
<p className="text-3xl font-bold text-slate-900 mb-1">{activeServices}</p>
|
||
<p className="text-sm text-slate-600">Active Services</p>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card className="border-slate-200">
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<div className={`w-10 h-10 ${complianceRate >= 95 ? 'bg-green-50' : 'bg-amber-50'} rounded-lg flex items-center justify-center`}>
|
||
<Shield className={`w-5 h-5 ${complianceRate >= 95 ? 'text-green-600' : 'text-amber-600'}`} />
|
||
</div>
|
||
<Badge variant="outline" className={complianceRate >= 95 ? 'text-green-700 border-green-200 bg-green-50' : 'text-amber-700 border-amber-200 bg-amber-50'}>
|
||
{complianceRate >= 95 ? "Compliant" : "Review"}
|
||
</Badge>
|
||
</div>
|
||
<p className="text-3xl font-bold text-slate-900 mb-1">{complianceRate}%</p>
|
||
<p className="text-sm text-slate-600">Wage Compliance</p>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card className="border-slate-200">
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<div className="w-10 h-10 bg-purple-50 rounded-lg flex items-center justify-center">
|
||
<DollarSign className="w-5 h-5 text-purple-600" />
|
||
</div>
|
||
<Badge variant="outline" className="text-purple-700 border-purple-200 bg-purple-50">
|
||
Potential
|
||
</Badge>
|
||
</div>
|
||
<p className="text-3xl font-bold text-slate-900 mb-1">${(revenueOpportunities / 1000).toFixed(0)}K</p>
|
||
<p className="text-sm text-slate-600">Revenue Opportunity</p>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
|
||
{/* Executive Summary */}
|
||
<Card className="mb-8 border-slate-200">
|
||
<CardHeader className="border-b border-slate-100 bg-white">
|
||
<CardTitle className="text-lg font-semibold text-slate-900 flex items-center gap-2">
|
||
<FileText className="w-5 h-5 text-[#0A39DF]" />
|
||
Executive Summary
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="p-6">
|
||
<div className="space-y-4">
|
||
{topPerformers.length > 0 && (
|
||
<div className="flex items-start gap-4 p-4 bg-emerald-50 rounded-lg border border-emerald-100">
|
||
<div className="w-8 h-8 bg-emerald-100 rounded-lg flex items-center justify-center flex-shrink-0 mt-0.5">
|
||
<Award className="w-4 h-4 text-emerald-700" />
|
||
</div>
|
||
<div className="flex-1">
|
||
<h4 className="font-semibold text-slate-900 mb-1">Top Performer</h4>
|
||
<p className="text-sm text-slate-700 leading-relaxed">
|
||
<span className="font-medium">{topPerformers[0].name}</span> is performing excellently with {topPerformers[0].optimal} optimally-priced services, maintaining competitive market rates while ensuring profitability.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{underperformers.length > 0 && (
|
||
<div className="flex items-start gap-4 p-4 bg-amber-50 rounded-lg border border-amber-100">
|
||
<div className="w-8 h-8 bg-amber-100 rounded-lg flex items-center justify-center flex-shrink-0 mt-0.5">
|
||
<AlertTriangle className="w-4 h-4 text-amber-700" />
|
||
</div>
|
||
<div className="flex-1">
|
||
<h4 className="font-semibold text-slate-900 mb-1">Action Required</h4>
|
||
<p className="text-sm text-slate-700 leading-relaxed">
|
||
<span className="font-medium">{underperformers[0].name}</span> requires attention
|
||
{underperformers[0].complianceIssues > 0 ? (
|
||
<span> — {underperformers[0].complianceIssues} compliance {underperformers[0].complianceIssues === 1 ? 'issue' : 'issues'} detected and must be addressed immediately.</span>
|
||
) : (
|
||
<span> for pricing strategy optimization to improve market competitiveness.</span>
|
||
)}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{complianceIssues.length > 0 && (
|
||
<div className="flex items-start gap-4 p-4 bg-red-50 rounded-lg border border-red-100">
|
||
<div className="w-8 h-8 bg-red-100 rounded-lg flex items-center justify-center flex-shrink-0 mt-0.5">
|
||
<Shield className="w-4 h-4 text-red-700" />
|
||
</div>
|
||
<div className="flex-1">
|
||
<h4 className="font-semibold text-slate-900 mb-1">Compliance Alert</h4>
|
||
<p className="text-sm text-slate-700 leading-relaxed">
|
||
{complianceIssues.length} {complianceIssues.length === 1 ? 'vendor has' : 'vendors have'} wage compliance issues below the California minimum wage ($16.50/hr) requiring immediate correction.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{underchargingVendors.length > 0 && (
|
||
<div className="flex items-start gap-4 p-4 bg-blue-50 rounded-lg border border-blue-100">
|
||
<div className="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center flex-shrink-0 mt-0.5">
|
||
<DollarSign className="w-4 h-4 text-blue-700" />
|
||
</div>
|
||
<div className="flex-1">
|
||
<h4 className="font-semibold text-slate-900 mb-1">Revenue Opportunity</h4>
|
||
<p className="text-sm text-slate-700 leading-relaxed">
|
||
${(revenueOpportunities / 1000).toFixed(0)}K in potential annual revenue identified through price optimization across {underchargingVendors.length} vendors with {underchargingVendors.reduce((sum, v) => sum + v.underpriced, 0)} underpriced services.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div className="flex items-start gap-4 p-4 bg-slate-50 rounded-lg border border-slate-100">
|
||
<div className="w-8 h-8 bg-slate-100 rounded-lg flex items-center justify-center flex-shrink-0 mt-0.5">
|
||
<Activity className="w-4 h-4 text-slate-700" />
|
||
</div>
|
||
<div className="flex-1">
|
||
<h4 className="font-semibold text-slate-900 mb-1">Overall Status</h4>
|
||
<p className="text-sm text-slate-700 leading-relaxed">
|
||
Network health is {complianceRate >= 95 ? "excellent" : "stable"} with {activeServices} active services across {totalVendors} vendors. {complianceRate >= 95 ? "Continue monitoring for sustained performance." : "Recommend focusing on compliance corrections and pricing optimization."}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Top Performers */}
|
||
{topPerformers.length > 0 && (
|
||
<Card className="mb-8 border-slate-200">
|
||
<CardHeader className="border-b border-slate-100 bg-white">
|
||
<div className="flex items-center justify-between">
|
||
<CardTitle className="text-lg font-semibold text-slate-900 flex items-center gap-2">
|
||
<Award className="w-5 h-5 text-emerald-600" />
|
||
Top Performing Vendors
|
||
</CardTitle>
|
||
<Badge className="bg-emerald-50 text-emerald-700 border-emerald-200">
|
||
{topPerformers.length} Vendors
|
||
</Badge>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent className="p-0">
|
||
<div className="divide-y divide-slate-100">
|
||
{topPerformers.map((vendor, idx) => (
|
||
<div key={idx} className="p-6 hover:bg-slate-50 transition-colors">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-4 flex-1">
|
||
<div className="w-12 h-12 bg-emerald-100 rounded-lg flex items-center justify-center font-bold text-emerald-700">
|
||
#{idx + 1}
|
||
</div>
|
||
<div className="flex-1">
|
||
<h4 className="font-semibold text-slate-900 mb-2">{vendor.name}</h4>
|
||
<div className="flex items-center gap-4 text-sm">
|
||
<div className="flex items-center gap-1 text-slate-600">
|
||
<CheckCircle2 className="w-4 h-4 text-emerald-600" />
|
||
<span>{vendor.optimal} Optimal</span>
|
||
</div>
|
||
<div className="flex items-center gap-1 text-slate-600">
|
||
<Activity className="w-4 h-4 text-blue-600" />
|
||
<span>{vendor.activeServices} Active</span>
|
||
</div>
|
||
<div className="flex items-center gap-1 text-slate-600">
|
||
<TrendingUp className="w-4 h-4 text-purple-600" />
|
||
<span>{vendor.avgMarkup}% Markup</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-3">
|
||
<div className="text-right">
|
||
<p className="text-2xl font-bold text-emerald-600">A+</p>
|
||
<p className="text-xs text-slate-500">Performance</p>
|
||
</div>
|
||
<Button variant="ghost" size="icon" className="text-slate-400 hover:text-slate-600">
|
||
<ChevronRight className="w-5 h-5" />
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
|
||
{/* Action Required */}
|
||
{underperformers.length > 0 && (
|
||
<Card className="mb-8 border-amber-200 bg-amber-50/30">
|
||
<CardHeader className="border-b border-amber-100 bg-white">
|
||
<div className="flex items-center justify-between">
|
||
<CardTitle className="text-lg font-semibold text-slate-900 flex items-center gap-2">
|
||
<AlertTriangle className="w-5 h-5 text-amber-600" />
|
||
Attention Required
|
||
</CardTitle>
|
||
<Badge className="bg-amber-100 text-amber-700 border-amber-200">
|
||
{underperformers.length} Vendors
|
||
</Badge>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent className="p-0">
|
||
<div className="divide-y divide-amber-100">
|
||
{underperformers.map((vendor, idx) => (
|
||
<div key={idx} className="p-6 hover:bg-amber-50 transition-colors">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-4 flex-1">
|
||
<div className="w-12 h-12 bg-amber-100 rounded-lg flex items-center justify-center">
|
||
<AlertTriangle className="w-6 h-6 text-amber-700" />
|
||
</div>
|
||
<div className="flex-1">
|
||
<h4 className="font-semibold text-slate-900 mb-2">{vendor.name}</h4>
|
||
<div className="flex items-center gap-3 flex-wrap">
|
||
{vendor.complianceIssues > 0 && (
|
||
<Badge variant="outline" className="bg-red-50 text-red-700 border-red-200">
|
||
<AlertCircle className="w-3 h-3 mr-1" />
|
||
{vendor.complianceIssues} Compliance {vendor.complianceIssues === 1 ? 'Issue' : 'Issues'}
|
||
</Badge>
|
||
)}
|
||
{vendor.underpriced > vendor.optimal && (
|
||
<Badge variant="outline" className="bg-orange-50 text-orange-700 border-orange-200">
|
||
<TrendingDown className="w-3 h-3 mr-1" />
|
||
{vendor.underpriced} Underpriced
|
||
</Badge>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<Button
|
||
onClick={() => handleReviewVendor(vendor)}
|
||
className="bg-[#0A39DF] hover:bg-[#0A39DF]/90 text-white"
|
||
>
|
||
Review
|
||
<ArrowRight className="w-4 h-4 ml-2" />
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
|
||
{/* Revenue Opportunities */}
|
||
{underchargingVendors.length > 0 && (
|
||
<Card className="mb-8 border-slate-200">
|
||
<CardHeader className="border-b border-slate-100 bg-white">
|
||
<div className="flex items-center justify-between">
|
||
<CardTitle className="text-lg font-semibold text-slate-900 flex items-center gap-2">
|
||
<DollarSign className="w-5 h-5 text-blue-600" />
|
||
Revenue Optimization Opportunities
|
||
</CardTitle>
|
||
<div className="text-right">
|
||
<p className="text-2xl font-bold text-blue-600">${(revenueOpportunities / 1000).toFixed(0)}K</p>
|
||
<p className="text-xs text-slate-500">Annual Potential</p>
|
||
</div>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent className="p-0">
|
||
<div className="divide-y divide-slate-100">
|
||
{underchargingVendors.map((vendor, idx) => (
|
||
<div key={idx} className="p-6 hover:bg-slate-50 transition-colors">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-4 flex-1">
|
||
<div className="w-12 h-12 bg-blue-50 rounded-lg flex items-center justify-center">
|
||
<DollarSign className="w-6 h-6 text-blue-600" />
|
||
</div>
|
||
<div className="flex-1">
|
||
<h4 className="font-semibold text-slate-900 mb-2">{vendor.name}</h4>
|
||
<p className="text-sm text-slate-600 mb-3">
|
||
{vendor.underpriced} services below market rate
|
||
</p>
|
||
<div className="flex items-center gap-3">
|
||
<div className="flex-1 bg-slate-200 rounded-full h-2 max-w-xs">
|
||
<div
|
||
className="bg-blue-600 h-full rounded-full transition-all duration-1000"
|
||
style={{ width: `${Math.min((vendor.underpriced / 10) * 100, 100)}%` }}
|
||
/>
|
||
</div>
|
||
<span className="text-sm font-semibold text-blue-600">
|
||
${(vendor.underpriced * 5000 / 1000).toFixed(0)}K
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<Button variant="outline" className="border-blue-200 text-blue-600 hover:bg-blue-50">
|
||
Optimize
|
||
<ChevronRight className="w-4 h-4 ml-2" />
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
<div className="p-6 bg-blue-50 border-t border-blue-100">
|
||
<div className="flex items-start gap-3">
|
||
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center flex-shrink-0">
|
||
<BarChart3 className="w-5 h-5 text-blue-600" />
|
||
</div>
|
||
<div>
|
||
<h5 className="font-semibold text-slate-900 mb-1">Recommendation</h5>
|
||
<p className="text-sm text-slate-700">
|
||
Adjusting {underchargingVendors.reduce((sum, v) => sum + v.underpriced, 0)} underpriced services to competitive market rates could generate ${(revenueOpportunities / 1000).toFixed(0)}K in additional annual revenue without impacting client competitiveness.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
|
||
{/* Compliance Issues */}
|
||
{complianceIssues.length > 0 && (
|
||
<Card className="border-red-200 bg-red-50/30">
|
||
<CardHeader className="border-b border-red-100 bg-white">
|
||
<div className="flex items-center justify-between">
|
||
<CardTitle className="text-lg font-semibold text-slate-900 flex items-center gap-2">
|
||
<Shield className="w-5 h-5 text-red-600" />
|
||
Compliance Violations
|
||
</CardTitle>
|
||
<Badge className="bg-red-100 text-red-700 border-red-200">
|
||
Critical
|
||
</Badge>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent className="p-0">
|
||
<div className="divide-y divide-red-100">
|
||
{complianceIssues.map((vendor, idx) => (
|
||
<div key={idx} className="p-6 hover:bg-red-50 transition-colors">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-4 flex-1">
|
||
<div className="w-12 h-12 bg-red-100 rounded-lg flex items-center justify-center">
|
||
<AlertTriangle className="w-6 h-6 text-red-700" />
|
||
</div>
|
||
<div className="flex-1">
|
||
<h4 className="font-semibold text-slate-900 mb-2">{vendor.name}</h4>
|
||
<p className="text-sm text-slate-700 mb-2">
|
||
<span className="font-medium text-red-600">{vendor.complianceIssues}</span> {vendor.complianceIssues === 1 ? 'service' : 'services'} below minimum wage ($16.50/hr)
|
||
</p>
|
||
<Badge variant="outline" className="bg-red-50 text-red-700 border-red-200">
|
||
<AlertCircle className="w-3 h-3 mr-1" />
|
||
Legal Risk: High
|
||
</Badge>
|
||
</div>
|
||
</div>
|
||
<Button className="bg-red-600 hover:bg-red-700 text-white">
|
||
Fix Now
|
||
<ArrowRight className="w-4 h-4 ml-2" />
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
<div className="p-6 bg-red-50 border-t border-red-100">
|
||
<div className="flex items-start gap-3">
|
||
<div className="w-10 h-10 bg-red-100 rounded-lg flex items-center justify-center flex-shrink-0">
|
||
<Shield className="w-5 h-5 text-red-600" />
|
||
</div>
|
||
<div>
|
||
<h5 className="font-semibold text-slate-900 mb-1">Legal Risk Assessment</h5>
|
||
<p className="text-sm text-slate-700">
|
||
These services must be corrected immediately to comply with California minimum wage laws ($16.50/hr). Non-compliance may result in penalties, lawsuits, and reputational damage.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
</>
|
||
);
|
||
};
|
||
|
||
const renderClientReports = () => {
|
||
const totalSpend = filteredEvents.reduce((sum, e) => sum + (e.total || 0), 0);
|
||
const avgOrderSize = filteredEvents.length > 0 ? totalSpend / filteredEvents.length : 0;
|
||
const completedOrders = filteredEvents.filter(e => e.status === "Completed").length;
|
||
const completionRate = filteredEvents.length > 0 ? ((completedOrders / filteredEvents.length) * 100).toFixed(1) : 0;
|
||
|
||
// Monthly spending trend
|
||
const monthlySpendMap = filteredEvents.reduce((acc, event) => {
|
||
if (!event.date) return acc;
|
||
try {
|
||
const date = new Date(event.date);
|
||
if (isNaN(date.getTime())) return acc; // Skip invalid dates
|
||
const monthYear = format(date, 'MMM yyyy');
|
||
if (!acc[monthYear]) acc[monthYear] = 0;
|
||
acc[monthYear] += event.total || 0;
|
||
} catch {
|
||
// Skip invalid dates
|
||
}
|
||
return acc;
|
||
}, {});
|
||
|
||
const spendingTrend = Object.entries(monthlySpendMap)
|
||
.sort(([a], [b]) => new Date(a).getTime() - new Date(b).getTime()) // Sort by date
|
||
.map(([month, spend]) => ({ month, spend: spend }))
|
||
.slice(-6); // Last 6 months
|
||
|
||
// Order status breakdown
|
||
const statusBreakdown = filteredEvents.reduce((acc, event) => {
|
||
const status = event.status || "Unknown";
|
||
acc[status] = (acc[status] || 0) + 1;
|
||
return acc;
|
||
}, {});
|
||
|
||
const statusData = Object.entries(statusBreakdown).map(([name, value]) => ({
|
||
name,
|
||
value
|
||
}));
|
||
|
||
return (
|
||
<>
|
||
{/* Client Metrics */}
|
||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||
<Card className="border-slate-200">
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<div className="w-10 h-10 bg-blue-50 rounded-lg flex items-center justify-center">
|
||
<Calendar className="w-5 h-5 text-blue-600" />
|
||
</div>
|
||
<Badge className="bg-blue-100 text-blue-700">Total</Badge>
|
||
</div>
|
||
<p className="text-3xl font-bold text-slate-900 mb-1">{filteredEvents.length}</p>
|
||
<p className="text-sm text-slate-600">Total Orders</p>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card className="border-slate-200">
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<div className="w-10 h-10 bg-green-50 rounded-lg flex items-center justify-center">
|
||
<DollarSign className="w-5 h-5 text-green-600" />
|
||
</div>
|
||
<Badge className="bg-green-100 text-green-700">Spend</Badge>
|
||
</div>
|
||
<p className="text-3xl font-bold text-slate-900 mb-1">${(totalSpend / 1000).toFixed(1)}K</p>
|
||
<p className="text-sm text-slate-600">Total Spend</p>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card className="border-slate-200">
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<div className="w-10 h-10 bg-purple-50 rounded-lg flex items-center justify-center">
|
||
<TrendingUp className="w-5 h-5 text-purple-600" />
|
||
</div>
|
||
<Badge className="bg-purple-100 text-purple-700">Average</Badge>
|
||
</div>
|
||
<p className="text-3xl font-bold text-slate-900 mb-1">${avgOrderSize.toFixed(0)}</p>
|
||
<p className="text-sm text-slate-600">Avg Order Size</p>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card className="border-slate-200">
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<div className="w-10 h-10 bg-emerald-50 rounded-lg flex items-center justify-center">
|
||
<CheckCircle2 className="w-5 h-5 text-emerald-600" />
|
||
</div>
|
||
<Badge className="bg-emerald-100 text-emerald-700">{completionRate}%</Badge>
|
||
</div>
|
||
<p className="text-3xl font-bold text-slate-900 mb-1">{completedOrders}</p>
|
||
<p className="text-sm text-slate-600">Completed Orders</p>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
|
||
{/* Client Charts */}
|
||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
|
||
<Card className="border-slate-200">
|
||
<CardHeader className="border-b bg-gradient-to-br from-slate-50 to-white">
|
||
<CardTitle className="text-[#1C323E]">Monthly Spending Trend</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="p-6">
|
||
<ResponsiveContainer width="100%" height={300}>
|
||
<AreaChart data={spendingTrend}>
|
||
<CartesianGrid strokeDasharray="3 3" />
|
||
<XAxis dataKey="month" />
|
||
<YAxis />
|
||
<Tooltip formatter={(value) => `$${value.toFixed(2)}`} />
|
||
<Area type="monotone" dataKey="spend" stroke="#0A39DF" fill="#0A39DF" fillOpacity={0.3} />
|
||
</AreaChart>
|
||
</ResponsiveContainer>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card className="border-slate-200">
|
||
<CardHeader className="border-b bg-gradient-to-br from-slate-50 to-white">
|
||
<CardTitle className="text-[#1C323E]">Order Status Breakdown</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="p-6">
|
||
<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 />
|
||
<Legend />
|
||
</PieChart>
|
||
</ResponsiveContainer>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
</>
|
||
);
|
||
};
|
||
|
||
const renderVendorReports = () => {
|
||
const totalOrders = filteredEvents.length;
|
||
const completedOrders = filteredEvents.filter(e => e.status === "Completed").length;
|
||
const fillRate = totalOrders > 0 ? ((completedOrders / totalOrders) * 100).toFixed(1) : 0;
|
||
const totalRevenue = filteredEvents.reduce((sum, e) => sum + (e.total || 0), 0);
|
||
const avgRating = filteredStaff.length > 0 ? (filteredStaff.reduce((sum, s) => sum + (s.rating || 0), 0) / filteredStaff.length).toFixed(1) : 0;
|
||
|
||
return (
|
||
<>
|
||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||
<Card className="border-slate-200">
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<div className="w-10 h-10 bg-blue-50 rounded-lg flex items-center justify-center">
|
||
<Package className="w-5 h-5 text-blue-600" />
|
||
</div>
|
||
<Badge className="bg-blue-100 text-blue-700">Orders</Badge>
|
||
</div>
|
||
<p className="text-3xl font-bold text-slate-900 mb-1">{totalOrders}</p>
|
||
<p className="text-sm text-slate-600">Total Orders</p>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card className="border-slate-200">
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<div className="w-10 h-10 bg-green-50 rounded-lg flex items-center justify-center">
|
||
<CheckCircle2 className="w-5 h-5 text-green-600" />
|
||
</div>
|
||
<Badge className="bg-green-100 text-green-700">{fillRate}%</Badge>
|
||
</div>
|
||
<p className="text-3xl font-bold text-slate-900 mb-1">{fillRate}%</p>
|
||
<p className="text-sm text-slate-600">Fill Rate</p>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card className="border-slate-200">
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<div className="w-10 h-10 bg-purple-50 rounded-lg flex items-center justify-center">
|
||
<DollarSign className="w-5 h-5 text-purple-600" />
|
||
</div>
|
||
<Badge className="bg-purple-100 text-purple-700">Revenue</Badge>
|
||
</div>
|
||
<p className="text-3xl font-bold text-slate-900 mb-1">${(totalRevenue / 1000).toFixed(1)}K</p>
|
||
<p className="text-sm text-slate-600">Total Revenue</p>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card className="border-slate-200">
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<div className="w-10 h-10 bg-amber-50 rounded-lg flex items-center justify-center">
|
||
<Award className="w-5 h-5 text-amber-600" />
|
||
</div>
|
||
<Badge className="bg-amber-100 text-amber-700">{avgRating}/5</Badge>
|
||
</div>
|
||
<p className="text-3xl font-bold text-slate-900 mb-1">{avgRating}</p>
|
||
<p className="text-sm text-slate-600">Avg Staff Rating</p>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
</>
|
||
);
|
||
};
|
||
|
||
const renderWorkforceReports = () => {
|
||
// For workforce, assuming filteredStaff contains only the current user's staff data
|
||
const myStats = filteredStaff.find(s => s.employee_name === user?.full_name || s.created_by === user?.email) || {};
|
||
const totalShifts = myStats.total_shifts || 0;
|
||
const myRating = myStats.rating || 0;
|
||
const coverage = myStats.shift_coverage_percentage || 0;
|
||
const cancellations = myStats.cancellation_count || 0;
|
||
|
||
return (
|
||
<>
|
||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||
<Card className="border-slate-200">
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<div className="w-10 h-10 bg-blue-50 rounded-lg flex items-center justify-center">
|
||
<Calendar className="w-5 h-5 text-blue-600" />
|
||
</div>
|
||
<Badge className="bg-blue-100 text-blue-700">Shifts</Badge>
|
||
</div>
|
||
<p className="text-3xl font-bold text-slate-900 mb-1">{totalShifts}</p>
|
||
<p className="text-sm text-slate-600">Total Shifts</p>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card className="border-slate-200">
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<div className="w-10 h-10 bg-amber-50 rounded-lg flex items-center justify-center">
|
||
<Award className="w-5 h-5 text-amber-600" />
|
||
</div>
|
||
<Badge className="bg-amber-100 text-amber-700">{myRating}/5</Badge>
|
||
</div>
|
||
<p className="text-3xl font-bold text-slate-900 mb-1">{myRating}</p>
|
||
<p className="text-sm text-slate-600">My Rating</p>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card className="border-slate-200">
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<div className="w-10 h-10 bg-green-50 rounded-lg flex items-center justify-center">
|
||
<CheckCircle2 className="w-5 h-5 text-green-600" />
|
||
</div>
|
||
<Badge className="bg-green-100 text-green-700">{coverage}%</Badge>
|
||
</div>
|
||
<p className="text-3xl font-bold text-slate-900 mb-1">{coverage}%</p>
|
||
<p className="text-sm text-slate-600">Coverage Rate</p>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card className="border-slate-200">
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<div className="w-10 h-10 bg-red-50 rounded-lg flex items-center justify-center">
|
||
<AlertCircle className="w-5 h-5 text-red-600" />
|
||
</div>
|
||
<Badge className="bg-red-100 text-red-700">Total</Badge>
|
||
</div>
|
||
<p className="text-3xl font-bold text-slate-900 mb-1">{cancellations}</p>
|
||
<p className="text-sm text-slate-600">Cancellations</p>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
</>
|
||
);
|
||
};
|
||
|
||
const renderOperatorSectorReports = () => {
|
||
const totalOrders = filteredEvents.length;
|
||
const activeOrders = filteredEvents.filter(e => e.status === "Active" || e.status === "Confirmed").length;
|
||
const completedOrders = filteredEvents.filter(e => e.status === "Completed").length;
|
||
const totalStaff = filteredStaff.length;
|
||
const avgCoverage = filteredStaff.length > 0 ? Math.round(filteredStaff.reduce((sum, s) => sum + (s.shift_coverage_percentage || 0), 0) / filteredStaff.length) : 0;
|
||
|
||
return (
|
||
<>
|
||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||
<Card className="border-slate-200">
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<div className="w-10 h-10 bg-blue-50 rounded-lg flex items-center justify-center">
|
||
<Calendar className="w-5 h-5 text-blue-600" />
|
||
</div>
|
||
<Badge className="bg-blue-100 text-blue-700">Total</Badge>
|
||
</div>
|
||
<p className="text-3xl font-bold text-slate-900 mb-1">{totalOrders}</p>
|
||
<p className="text-sm text-slate-600">Total Orders</p>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card className="border-slate-200">
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<div className="w-10 h-10 bg-amber-50 rounded-lg flex items-center justify-center">
|
||
<Clock className="w-5 h-5 text-amber-600" />
|
||
</div>
|
||
<Badge className="bg-amber-100 text-amber-700">Active</Badge>
|
||
</div>
|
||
<p className="text-3xl font-bold text-slate-900 mb-1">{activeOrders}</p>
|
||
<p className="text-sm text-slate-600">Active Orders</p>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card className="border-slate-200">
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<div className="w-10 h-10 bg-green-50 rounded-lg flex items-center justify-center">
|
||
<CheckCircle2 className="w-5 h-5 text-green-600" />
|
||
</div>
|
||
<Badge className="bg-green-100 text-green-700">Done</Badge>
|
||
</div>
|
||
<p className="text-3xl font-bold text-slate-900 mb-1">{completedOrders}</p>
|
||
<p className="text-sm text-slate-600">Completed</p>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card className="border-slate-200">
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<div className="w-10 h-10 bg-purple-50 rounded-lg flex items-center justify-center">
|
||
<Users className="w-5 h-5 text-purple-600" />
|
||
</div>
|
||
<Badge className="bg-purple-100 text-purple-700">{avgCoverage}%</Badge>
|
||
</div>
|
||
<p className="text-3xl font-bold text-slate-900 mb-1">{totalStaff}</p>
|
||
<p className="text-sm text-slate-600">Total Staff</p>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
</>
|
||
);
|
||
};
|
||
|
||
return (
|
||
<div className="min-h-screen bg-slate-50">
|
||
<div className="p-6 md:p-8 max-w-7xl mx-auto">
|
||
{/* Header */}
|
||
<div className="mb-8">
|
||
<div className="flex items-center justify-between gap-3 mb-2">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-10 h-10 bg-[#0A39DF] rounded-lg flex items-center justify-center">
|
||
<BarChart3 className="w-5 h-5 text-white" />
|
||
</div>
|
||
<div>
|
||
<h1 className="text-2xl font-bold text-slate-900">{greeting}</h1>
|
||
<p className="text-sm text-slate-500 flex items-center gap-2">
|
||
Data as of {safeFormatDate(new Date(), 'MMM dd, yyyy, h:mm a')}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Right Side: Action Icons + User Menu */}
|
||
<div className="flex items-center gap-6">
|
||
{/* Action Icons */}
|
||
<div className="flex items-center gap-2">
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
onClick={() => window.location.reload()}
|
||
className="text-[#0A39DF] hover:bg-blue-50"
|
||
title="Refresh"
|
||
>
|
||
<RefreshCw className="w-5 h-5" />
|
||
</Button>
|
||
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
onClick={() => window.print()}
|
||
className="text-[#0A39DF] hover:bg-blue-50"
|
||
title="Print"
|
||
>
|
||
<Printer className="w-5 h-5" />
|
||
</Button>
|
||
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
onClick={() => {
|
||
const subject = `${greeting} - KROW Reports`;
|
||
const body = `Check out the latest reports from KROW: ${window.location.href}`;
|
||
window.location.href = `mailto:?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
|
||
}}
|
||
className="text-[#0A39DF] hover:bg-blue-50"
|
||
title="Email"
|
||
>
|
||
<Mail className="w-5 h-5" />
|
||
</Button>
|
||
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
onClick={() => {
|
||
alert("Export functionality coming soon!");
|
||
}}
|
||
className="text-[#0A39DF] hover:bg-blue-50"
|
||
title="Download"
|
||
>
|
||
<Download className="w-5 h-5" />
|
||
</Button>
|
||
</div>
|
||
|
||
{/* Divider */}
|
||
<div className="h-8 w-px bg-slate-300"></div>
|
||
|
||
{/* User Profile */}
|
||
<button className="flex items-center gap-2 hover:opacity-70 transition-opacity">
|
||
<div className="w-10 h-10 rounded-full border-2 border-slate-300 flex items-center justify-center text-slate-600">
|
||
<User className="w-5 h-5" />
|
||
</div>
|
||
<span className="font-medium text-slate-700 hidden md:block">
|
||
{user?.full_name || user?.email?.split('@')[0] || 'User'}
|
||
</span>
|
||
</button>
|
||
|
||
{/* Menu Icon */}
|
||
<button className="flex items-center justify-center w-10 h-10 hover:bg-slate-100 rounded-lg transition-colors">
|
||
<Menu className="w-6 h-6 text-slate-600" />
|
||
</button>
|
||
|
||
{/* Help */}
|
||
<Link to={createPageUrl("Support")}>
|
||
<button className="flex items-center gap-2 hover:opacity-70 transition-opacity">
|
||
<div className="w-10 h-10 rounded-full border-2 border-slate-300 flex items-center justify-center text-slate-600">
|
||
<HelpCircle className="w-5 h-5" />
|
||
</div>
|
||
<span className="font-medium text-slate-700 hidden md:block">Help</span>
|
||
</button>
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Role-specific reports */}
|
||
{(userRole === "admin" || userRole === "procurement") && renderAdminProcurementReports()}
|
||
{userRole === "client" && renderClientReports()}
|
||
{userRole === "vendor" && renderVendorReports()}
|
||
{userRole === "workforce" && renderWorkforceReports()}
|
||
{(userRole === "operator" || userRole === "sector") && renderOperatorSectorReports()}
|
||
</div>
|
||
|
||
{/* Review Vendor Dialog */}
|
||
<Dialog open={showReviewDialog} onOpenChange={setShowReviewDialog}>
|
||
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||
<DialogHeader>
|
||
<DialogTitle className="text-2xl flex items-center gap-2">
|
||
<AlertTriangle className="w-6 h-6 text-amber-600" />
|
||
Review Required: {reviewVendor?.name}
|
||
</DialogTitle>
|
||
<DialogDescription>
|
||
Review and fix the issues below to optimize vendor performance
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
|
||
{reviewVendor && (
|
||
<div className="space-y-6 mt-4">
|
||
{/* Compliance Issues */}
|
||
{reviewVendor.complianceIssueServices.length > 0 && (
|
||
<div>
|
||
<div className="flex items-center gap-2 mb-4">
|
||
<div className="w-8 h-8 bg-red-100 rounded-lg flex items-center justify-center">
|
||
<Shield className="w-4 h-4 text-red-600" />
|
||
</div>
|
||
<h3 className="font-semibold text-lg text-slate-900">
|
||
Compliance Issues ({reviewVendor.complianceIssueServices.length})
|
||
</h3>
|
||
</div>
|
||
<div className="space-y-3">
|
||
{reviewVendor.complianceIssueServices.map((service, idx) => (
|
||
<div key={idx} className="p-4 bg-red-50 border border-red-200 rounded-lg">
|
||
<div className="flex items-start justify-between">
|
||
<div className="flex-1">
|
||
<div className="flex items-center gap-2 mb-2">
|
||
<h4 className="font-semibold text-slate-900">{service.role_name}</h4>
|
||
<Badge className="bg-red-600 text-white">{service.category}</Badge>
|
||
</div>
|
||
<div className="grid grid-cols-3 gap-4 text-sm mt-3">
|
||
<div>
|
||
<p className="text-slate-600">Current Wage</p>
|
||
<p className="font-bold text-red-700">${service.currentWage.toFixed(2)}/hr</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-slate-600">Minimum Required</p>
|
||
<p className="font-bold text-green-700">${service.requiredWage.toFixed(2)}/hr</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-slate-600">Increase Needed</p>
|
||
<p className="font-bold text-slate-900">+${service.difference}/hr</p>
|
||
</div>
|
||
</div>
|
||
<div className="mt-3 p-3 bg-white rounded border border-red-100">
|
||
<p className="text-xs font-semibold text-red-700 mb-1">⚠️ Legal Risk</p>
|
||
<p className="text-xs text-slate-700">
|
||
This wage is below California minimum wage ($16.50/hr). Immediate correction required to avoid penalties.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<Link to={createPageUrl("VendorRateCard")}>
|
||
<Button size="sm" className="ml-4 bg-red-600 hover:bg-red-700">
|
||
<Edit className="w-4 h-4 mr-1" />
|
||
Fix Now
|
||
</Button>
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Underpriced Services */}
|
||
{reviewVendor.underpricedServices.length > 0 && (
|
||
<div>
|
||
<div className="flex items-center gap-2 mb-4">
|
||
<div className="w-8 h-8 bg-orange-100 rounded-lg flex items-center justify-center">
|
||
<DollarSign className="w-4 h-4 text-orange-600" />
|
||
</div>
|
||
<h3 className="font-semibold text-lg text-slate-900">
|
||
Underpriced Services ({reviewVendor.underpricedServices.length})
|
||
</h3>
|
||
</div>
|
||
<div className="space-y-3">
|
||
{reviewVendor.underpricedServices.map((service, idx) => (
|
||
<div key={idx} className="p-4 bg-orange-50 border border-orange-200 rounded-lg">
|
||
<div className="flex items-start justify-between">
|
||
<div className="flex-1">
|
||
<div className="flex items-center gap-2 mb-2">
|
||
<h4 className="font-semibold text-slate-900">{service.role_name}</h4>
|
||
<Badge className="bg-orange-600 text-white">{service.category}</Badge>
|
||
</div>
|
||
<div className="grid grid-cols-3 gap-4 text-sm mt-3">
|
||
<div>
|
||
<p className="text-slate-600">Current Rate</p>
|
||
<p className="font-bold text-orange-700">${service.currentRate.toFixed(2)}/hr</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-slate-600">Market Average</p>
|
||
<p className="font-bold text-blue-700">${service.marketAverage.toFixed(2)}/hr</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-slate-600">Below Market</p>
|
||
<p className="font-bold text-red-700">{service.percentageBelow}%</p>
|
||
</div>
|
||
</div>
|
||
<div className="mt-3 p-3 bg-white rounded border border-orange-100">
|
||
<p className="text-xs font-semibold text-orange-700 mb-1">💰 Revenue Opportunity</p>
|
||
<p className="text-xs text-slate-700">
|
||
Adjusting to market rate could generate ~${service.potentialRevenue}/month additional revenue for this service.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<Link to={createPageUrl("VendorRateCard")}>
|
||
<Button size="sm" variant="outline" className="ml-4 border-orange-300 text-orange-700 hover:bg-orange-50">
|
||
<Edit className="w-4 h-4 mr-1" />
|
||
Optimize
|
||
</Button>
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Action Summary */}
|
||
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
||
<div className="flex items-start gap-3">
|
||
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center flex-shrink-0">
|
||
<Target className="w-5 h-5 text-blue-600" />
|
||
</div>
|
||
<div className="flex-1">
|
||
<h4 className="font-semibold text-slate-900 mb-2">Next Steps</h4>
|
||
<ul className="text-sm text-slate-700 space-y-1">
|
||
{reviewVendor.complianceIssueServices.length > 0 && (
|
||
<li className="flex items-center gap-2">
|
||
<div className="w-1.5 h-1.5 bg-red-600 rounded-full"></div>
|
||
<span>Fix {reviewVendor.complianceIssueServices.length} compliance {reviewVendor.complianceIssueServices.length === 1 ? 'issue' : 'issues'} immediately</span>
|
||
</li>
|
||
)}
|
||
{reviewVendor.underpricedServices.length > 0 && (
|
||
<li className="flex items-center gap-2">
|
||
<div className="w-1.5 h-1.5 bg-orange-600 rounded-full"></div>
|
||
<span>Review and adjust {reviewVendor.underpricedServices.length} underpriced {reviewVendor.underpricedServices.length === 1 ? 'service' : 'services'}</span>
|
||
</li>
|
||
)}
|
||
<li className="flex items-center gap-2">
|
||
<div className="w-1.5 h-1.5 bg-blue-600 rounded-full"></div>
|
||
<span>Update vendor rate card to reflect market standards</span>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Action Buttons */}
|
||
<div className="flex items-center justify-end gap-3 pt-4 border-t border-slate-200">
|
||
<Button
|
||
variant="outline"
|
||
onClick={() => setShowReviewDialog(false)}
|
||
>
|
||
Close
|
||
</Button>
|
||
<Link to={createPageUrl("VendorRateCard")}>
|
||
<Button className="bg-[#0A39DF] hover:bg-[#0A39DF]/90">
|
||
<Edit className="w-4 h-4 mr-2" />
|
||
Go to Vendor Rates
|
||
</Button>
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</DialogContent>
|
||
</Dialog>
|
||
</div>
|
||
);
|
||
}
|