Files
Krow-workspace/frontend-web/src/pages/Reports.jsx
bwnyasse 554dc9f9e3 feat: Initialize monorepo structure and comprehensive documentation
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.
2025-11-12 12:50:55 -05:00

1284 lines
58 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}