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.
740 lines
33 KiB
JavaScript
740 lines
33 KiB
JavaScript
import React, { useState, useMemo } from "react";
|
||
import { base44 } from "@/api/base44Client";
|
||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||
import { Link, useNavigate } from "react-router-dom";
|
||
import { createPageUrl } from "@/utils";
|
||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||
import { Badge } from "@/components/ui/badge";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Switch } from "@/components/ui/switch";
|
||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||
import { Award, TrendingUp, Users, DollarSign, CheckCircle2, Clock, UserCheck, Mail, Edit, Bot, ArrowRight, Rocket, Star, Brain, Check, Sparkles, Zap, Calendar, Package, MessageSquare, AlertTriangle, MapPin, BarChart3, RefreshCw, Download, X, Lightbulb, TrendingDown, Timer } from "lucide-react";
|
||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, BarChart, Bar } from 'recharts';
|
||
import { format, startOfMonth, endOfMonth, subMonths, differenceInHours, parseISO } from "date-fns";
|
||
import { useToast } from "@/components/ui/use-toast";
|
||
|
||
const mockWorkers = [
|
||
{ id: 1, name: "Maria G.", skill: "Server", rating: 4.9, distance: 3.2, reliability: 0.99, pro: 2 },
|
||
{ id: 2, name: "Andre P.", skill: "Culinary", rating: 4.8, distance: 5.5, reliability: 0.96, pro: 3 },
|
||
{ id: 3, name: "Jae L.", skill: "Dishwasher", rating: 4.6, distance: 1.1, reliability: 0.93, pro: 1 },
|
||
{ id: 4, name: "Rita C.", skill: "Server", rating: 4.7, distance: 4.0, reliability: 0.95, pro: 2 },
|
||
{ id: 5, name: "Luis M.", skill: "Culinary", rating: 4.5, distance: 7.4, reliability: 0.9, pro: 1 },
|
||
];
|
||
|
||
const proColors = {
|
||
1: "bg-slate-200 text-slate-800",
|
||
2: "bg-blue-100 text-blue-900",
|
||
3: "bg-amber-100 text-amber-900",
|
||
};
|
||
|
||
const formatPct = (v) => `${Math.round(v * 100)}%`;
|
||
|
||
function AINudge({ text, cta, onCta, type = "success", onDismiss }) {
|
||
const [dismissed, setDismissed] = useState(false);
|
||
|
||
const handleDismiss = () => {
|
||
setDismissed(true);
|
||
if (onDismiss) onDismiss();
|
||
};
|
||
|
||
if (dismissed) return null;
|
||
|
||
const typeConfig = {
|
||
success: {
|
||
icon: Sparkles,
|
||
gradient: "from-emerald-500 via-teal-500 to-cyan-500",
|
||
bgGradient: "from-emerald-50/80 via-teal-50/80 to-cyan-50/80",
|
||
iconBg: "from-emerald-400 to-teal-500",
|
||
textColor: "text-emerald-900",
|
||
subTextColor: "text-emerald-700",
|
||
buttonColor: "from-emerald-600 to-teal-600 hover:from-emerald-700 hover:to-teal-700",
|
||
borderColor: "border-emerald-200/60",
|
||
glowColor: "shadow-emerald-200/50"
|
||
},
|
||
insight: {
|
||
icon: Lightbulb,
|
||
gradient: "from-amber-500 via-orange-500 to-yellow-500",
|
||
bgGradient: "from-amber-50/80 via-orange-50/80 to-yellow-50/80",
|
||
iconBg: "from-amber-400 to-orange-500",
|
||
textColor: "text-amber-900",
|
||
subTextColor: "text-amber-700",
|
||
buttonColor: "from-amber-600 to-orange-600 hover:from-amber-700 hover:to-orange-700",
|
||
borderColor: "border-amber-200/60",
|
||
glowColor: "shadow-amber-200/50"
|
||
},
|
||
achievement: {
|
||
icon: Award,
|
||
gradient: "from-purple-500 via-pink-500 to-rose-500",
|
||
bgGradient: "from-purple-50/80 via-pink-50/80 to-rose-50/80",
|
||
iconBg: "from-purple-400 to-pink-500",
|
||
textColor: "text-purple-900",
|
||
subTextColor: "text-purple-700",
|
||
buttonColor: "from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700",
|
||
borderColor: "border-purple-200/60",
|
||
glowColor: "shadow-purple-200/50"
|
||
}
|
||
};
|
||
|
||
const config = typeConfig[type];
|
||
const IconComponent = config.icon;
|
||
|
||
return (
|
||
<div className={`relative overflow-hidden rounded-2xl bg-gradient-to-r ${config.bgGradient} backdrop-blur-sm border-2 ${config.borderColor} shadow-lg ${config.glowColor} transition-all duration-300 hover:shadow-xl hover:scale-[1.02]`}>
|
||
<div className={`absolute top-0 left-0 right-0 h-1 bg-gradient-to-r ${config.gradient}`} />
|
||
|
||
<div className="flex items-start gap-4 p-5">
|
||
<div className={`flex-shrink-0 w-12 h-12 rounded-xl bg-gradient-to-br ${config.iconBg} flex items-center justify-center shadow-lg`}>
|
||
<IconComponent className="w-6 h-6 text-white" />
|
||
</div>
|
||
|
||
<div className="flex-1 min-w-0 pt-1">
|
||
<div className="flex items-start gap-2 mb-2">
|
||
<Badge className={`bg-gradient-to-r ${config.gradient} text-white border-0 shadow-md text-xs font-bold px-2 py-0.5`}>
|
||
AI INSIGHT
|
||
</Badge>
|
||
</div>
|
||
<p className={`${config.textColor} font-semibold text-base leading-relaxed`}>
|
||
{text}
|
||
</p>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-2 flex-shrink-0">
|
||
{cta && (
|
||
<Button
|
||
size="sm"
|
||
onClick={onCta}
|
||
className={`bg-gradient-to-r ${config.buttonColor} text-white shadow-lg rounded-xl font-semibold px-4 py-2 transition-all duration-200`}
|
||
>
|
||
{cta}
|
||
<ArrowRight className="w-4 h-4 ml-2" />
|
||
</Button>
|
||
)}
|
||
<Button
|
||
size="icon"
|
||
variant="ghost"
|
||
onClick={handleDismiss}
|
||
className={`h-8 w-8 rounded-lg ${config.subTextColor} hover:bg-white/50 transition-colors`}
|
||
>
|
||
<X className="w-4 h-4" />
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default function VendorDashboard() {
|
||
const navigate = useNavigate();
|
||
const queryClient = useQueryClient();
|
||
const { toast } = useToast();
|
||
|
||
const { data: user } = useQuery({
|
||
queryKey: ['current-user-vendor'],
|
||
queryFn: () => base44.auth.me(),
|
||
});
|
||
|
||
const { data: events } = useQuery({
|
||
queryKey: ['vendor-events'],
|
||
queryFn: async () => {
|
||
const allEvents = await base44.entities.Event.list('-date');
|
||
if (!user?.email) return [];
|
||
return allEvents.filter(e =>
|
||
e.vendor_name === user?.company_name ||
|
||
e.vendor_id === user?.id ||
|
||
e.created_by === user?.email
|
||
);
|
||
},
|
||
initialData: [],
|
||
enabled: !!user
|
||
});
|
||
|
||
const { data: staff } = useQuery({
|
||
queryKey: ['vendor-staff'],
|
||
queryFn: async () => {
|
||
const allStaff = await base44.entities.Staff.list();
|
||
if (!user?.company_name) return allStaff.slice(0, 10);
|
||
return allStaff.filter(s => s.vendor_name === user?.company_name);
|
||
},
|
||
initialData: [],
|
||
enabled: !!user
|
||
});
|
||
|
||
// Calculations
|
||
const todayOrders = events.filter(e => {
|
||
const eventDate = new Date(e.date);
|
||
const today = new Date();
|
||
return eventDate.toDateString() === today.toDateString();
|
||
});
|
||
|
||
const activeOrders = events.filter(e => e.status === "Active" || e.status === "Confirmed" || e.status === "In Progress");
|
||
const completedOrders = events.filter(e => e.status === "Completed");
|
||
const totalRevenue = completedOrders.reduce((sum, e) => sum + (e.total || 0), 0);
|
||
|
||
const currentMonth = new Date().getMonth();
|
||
const currentYear = new Date().getFullYear();
|
||
const thisMonthOrders = events.filter(e => {
|
||
const eventDate = new Date(e.date);
|
||
return eventDate.getMonth() === currentMonth &&
|
||
eventDate.getFullYear() === currentYear &&
|
||
(e.status === "Completed" || e.status === "Active");
|
||
});
|
||
const thisMonthRevenue = thisMonthOrders.reduce((sum, e) => sum + (e.total || 0), 0);
|
||
|
||
const activeStaff = staff.filter(s => s.employment_type !== "Medical Leave" && s.action !== "Inactive").length;
|
||
const avgRating = staff.reduce((sum, s) => sum + (s.rating || 0), 0) / (staff.length || 1);
|
||
const reliabilityScore = staff.reduce((sum, s) => sum + (s.reliability_score || 0), 0) / (staff.length || 1);
|
||
|
||
// Rapid Orders (urgent orders within 24 hours)
|
||
const rapidOrders = events
|
||
.filter(e => {
|
||
const eventDate = new Date(e.date);
|
||
const now = new Date();
|
||
const hoursUntil = differenceInHours(eventDate, now);
|
||
return hoursUntil > 0 && hoursUntil <= 24 &&
|
||
(e.status === "Active" || e.status === "Confirmed" || e.status === "Pending");
|
||
})
|
||
.sort((a, b) => new Date(a.date) - new Date(b.date))
|
||
.slice(0, 3);
|
||
|
||
// 6-month revenue and payroll trend
|
||
const last6Months = Array.from({ length: 6 }, (_, i) => {
|
||
const date = subMonths(new Date(), 5 - i);
|
||
return {
|
||
month: format(date, 'MMM'),
|
||
fullDate: date
|
||
};
|
||
});
|
||
|
||
const salesPayrollTrend = last6Months.map(({ month, fullDate }) => {
|
||
const monthStart = startOfMonth(fullDate);
|
||
const monthEnd = endOfMonth(fullDate);
|
||
const monthOrders = events.filter(e => {
|
||
const eventDate = new Date(e.date);
|
||
return eventDate >= monthStart && eventDate <= monthEnd && e.status === "Completed";
|
||
});
|
||
const sales = monthOrders.reduce((sum, e) => sum + (e.total || 0), 0);
|
||
const payroll = sales * 0.68; // Assume 68% payroll ratio
|
||
return { month, sales, payroll };
|
||
});
|
||
|
||
// Top clients with fill rates
|
||
const clientRevenue = completedOrders.reduce((acc, event) => {
|
||
const client = event.business_name || "Unknown";
|
||
if (!acc[client]) {
|
||
acc[client] = { name: client, revenue: 0, orders: 0, requested: 0, assigned: 0 };
|
||
}
|
||
acc[client].revenue += (event.total || 0);
|
||
acc[client].orders++;
|
||
acc[client].requested += (event.requested || 0);
|
||
acc[client].assigned += (event.assigned_staff?.length || 0);
|
||
return acc;
|
||
}, {});
|
||
|
||
const topClients = Object.values(clientRevenue)
|
||
.map(c => ({
|
||
...c,
|
||
fillRate: c.requested > 0 ? (c.assigned / c.requested) * 100 : 0
|
||
}))
|
||
.sort((a, b) => b.revenue - a.revenue)
|
||
.slice(0, 4);
|
||
|
||
// Top performers with shift counts
|
||
const topPerformers = staff
|
||
.filter(s => s.rating > 0)
|
||
.sort((a, b) => (b.rating || 0) - (a.rating || 0))
|
||
.slice(0, 3)
|
||
.map(s => ({
|
||
...s,
|
||
shifts: s.total_shifts || Math.floor(Math.random() * 30) + 5
|
||
}));
|
||
|
||
// Staff assigned today
|
||
const staffAssignedToday = todayOrders.reduce((sum, e) => sum + (e.requested || 0), 0);
|
||
|
||
// Avg speed to fill (mock calculation)
|
||
const avgSpeedToFill = "1h 12m";
|
||
const speedChange = "-14m";
|
||
|
||
const hour = new Date().getHours();
|
||
const greeting = hour < 12 ? "Good morning" : hour < 18 ? "Good afternoon" : "Good evening";
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/20 to-slate-50">
|
||
<div className="max-w-[1800px] mx-auto p-6 space-y-6">
|
||
|
||
{/* Header */}
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<h1 className="text-3xl font-bold text-[#1C323E] mb-1">
|
||
{greeting}, {user?.full_name?.split(' ')[0] || 'Partner'}
|
||
</h1>
|
||
<p className="text-slate-600">Here's your performance overview</p>
|
||
</div>
|
||
<div className="flex items-center gap-3">
|
||
<Button variant="outline">
|
||
<RefreshCw className="w-4 h-4 mr-2" />
|
||
Refresh
|
||
</Button>
|
||
<Button className="bg-[#0A39DF] hover:bg-[#0A39DF]/90">
|
||
<Download className="w-4 h-4 mr-2" />
|
||
Export
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Top KPI Metrics - NEW DESIGN */}
|
||
<div className="grid grid-cols-4 gap-4">
|
||
{/* Sales */}
|
||
<Card className="bg-gradient-to-br from-white to-slate-50 border-slate-200 shadow-sm hover:shadow-md transition-all">
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center gap-3 mb-3">
|
||
<div className="w-10 h-10 bg-slate-100 rounded-lg flex items-center justify-center">
|
||
<TrendingUp className="w-5 h-5 text-slate-700" />
|
||
</div>
|
||
<div>
|
||
<p className="text-xs text-slate-500 font-medium">Sales (M1D)</p>
|
||
</div>
|
||
</div>
|
||
<p className="text-4xl font-bold text-[#1C323E] mb-1">
|
||
${Math.round(thisMonthRevenue / 1000)},000
|
||
</p>
|
||
<p className="text-sm text-emerald-600 font-medium">+5% vs last month</p>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Payroll */}
|
||
<Card className="bg-gradient-to-br from-white to-slate-50 border-slate-200 shadow-sm hover:shadow-md transition-all">
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center gap-3 mb-3">
|
||
<div className="w-10 h-10 bg-slate-100 rounded-lg flex items-center justify-center">
|
||
<DollarSign className="w-5 h-5 text-slate-700" />
|
||
</div>
|
||
<div>
|
||
<p className="text-xs text-slate-500 font-medium">Payroll (M1D)</p>
|
||
</div>
|
||
</div>
|
||
<p className="text-4xl font-bold text-[#1C323E] mb-1">
|
||
${Math.round(thisMonthRevenue * 0.68 / 1000)},000
|
||
</p>
|
||
<p className="text-sm text-slate-600 font-medium">68% of sales</p>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Active Workforce */}
|
||
<Card className="bg-gradient-to-br from-white to-slate-50 border-slate-200 shadow-sm hover:shadow-md transition-all">
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center gap-3 mb-3">
|
||
<div className="w-10 h-10 bg-slate-100 rounded-lg flex items-center justify-center">
|
||
<Users className="w-5 h-5 text-slate-700" />
|
||
</div>
|
||
<div>
|
||
<p className="text-xs text-slate-500 font-medium">Active Workforce</p>
|
||
</div>
|
||
</div>
|
||
<p className="text-4xl font-bold text-[#1C323E] mb-1">
|
||
{activeStaff}
|
||
</p>
|
||
<p className="text-sm text-slate-600 font-medium">+3 this week</p>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Avg Speed to Fill */}
|
||
<Card className="bg-gradient-to-br from-white to-slate-50 border-slate-200 shadow-sm hover:shadow-md transition-all">
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center gap-3 mb-3">
|
||
<div className="w-10 h-10 bg-slate-100 rounded-lg flex items-center justify-center">
|
||
<Timer className="w-5 h-5 text-slate-700" />
|
||
</div>
|
||
<div>
|
||
<p className="text-xs text-slate-500 font-medium">Avg Speed-to-Fill</p>
|
||
</div>
|
||
</div>
|
||
<p className="text-4xl font-bold text-[#1C323E] mb-1">
|
||
{avgSpeedToFill}
|
||
</p>
|
||
<p className="text-sm text-emerald-600 font-medium">{speedChange} vs last week</p>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
|
||
{/* AI Insights */}
|
||
<div className="space-y-4">
|
||
<AINudge
|
||
text="Assigned 6 top-fit workers. Consider adding 1 trainee for fairness and skill development."
|
||
cta="Add Trainee"
|
||
type="success"
|
||
onCta={() => {}}
|
||
/>
|
||
<AINudge
|
||
text="You're outperforming 82% of vendors in your region. Share your Gold badge to attract premium clients!"
|
||
cta="Share Badge"
|
||
type="achievement"
|
||
onCta={() => {}}
|
||
/>
|
||
</div>
|
||
|
||
{/* Main Content Grid */}
|
||
<div className="grid grid-cols-3 gap-6">
|
||
|
||
{/* Left Column (2 cols) */}
|
||
<div className="col-span-2 space-y-6">
|
||
|
||
{/* RAPID ORDERS - NEW SECTION */}
|
||
<Card className="bg-white border-slate-200 shadow-sm">
|
||
<CardHeader className="border-b border-slate-100 pb-4">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<CardTitle className="text-base flex items-center gap-2">
|
||
<Zap className="w-5 h-5 text-amber-500" />
|
||
Rapid Orders
|
||
</CardTitle>
|
||
<p className="text-xs text-slate-600 mt-1">Urgent orders within 24 hours</p>
|
||
</div>
|
||
{rapidOrders.length > 0 && (
|
||
<Badge className="bg-red-100 text-red-700 border-red-200 font-bold">
|
||
{rapidOrders.length} Urgent
|
||
</Badge>
|
||
)}
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent className="p-5">
|
||
{rapidOrders.length > 0 ? (
|
||
<div className="space-y-4">
|
||
{rapidOrders.map((order) => {
|
||
const eventDate = order.date ? parseISO(order.date) : new Date();
|
||
const hoursUntil = differenceInHours(eventDate, new Date());
|
||
const assignedCount = order.assigned_staff?.length || 0;
|
||
const requestedCount = order.requested || 0;
|
||
const fillPercentage = requestedCount > 0 ? Math.round((assignedCount / requestedCount) * 100) : 0;
|
||
|
||
return (
|
||
<div key={order.id} className="p-4 rounded-xl bg-gradient-to-r from-red-50 to-orange-50 border-2 border-red-200 hover:border-red-300 transition-all">
|
||
<div className="flex items-start justify-between mb-3">
|
||
<div>
|
||
<div className="flex items-center gap-2 mb-1">
|
||
<h3 className="font-bold text-[#1C323E]">{order.business_name || "Client"} – {order.event_name}</h3>
|
||
<Badge className="bg-red-500 text-white font-bold text-xs">
|
||
{hoursUntil}h left
|
||
</Badge>
|
||
</div>
|
||
<div className="flex items-center gap-4 text-sm text-slate-600">
|
||
<div className="flex items-center gap-1">
|
||
<Users className="w-4 h-4" />
|
||
<span>{assignedCount}/{requestedCount} filled</span>
|
||
</div>
|
||
<div className="flex items-center gap-1">
|
||
<Calendar className="w-4 h-4" />
|
||
<span>{format(eventDate, "h:mm a")}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="flex gap-2">
|
||
<Link to={createPageUrl(`EventDetail?id=${order.id}`)}>
|
||
<Button size="sm" variant="outline" className="rounded-lg">
|
||
View
|
||
</Button>
|
||
</Link>
|
||
<Button size="sm" className="bg-red-600 hover:bg-red-700 text-white rounded-lg font-semibold">
|
||
Quick Assign
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<div className="flex-1 h-2 bg-white rounded-full overflow-hidden">
|
||
<div
|
||
className={`h-full transition-all duration-500 ${
|
||
fillPercentage < 60 ? 'bg-red-500' :
|
||
fillPercentage < 90 ? 'bg-amber-500' :
|
||
'bg-green-500'
|
||
}`}
|
||
style={{ width: `${fillPercentage}%` }}
|
||
/>
|
||
</div>
|
||
<span className="text-sm font-bold text-slate-700">{fillPercentage}%</span>
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
) : (
|
||
<div className="text-center py-12">
|
||
<Zap className="w-12 h-12 mx-auto text-slate-300 mb-3" />
|
||
<p className="text-sm text-slate-500 font-medium">No urgent orders</p>
|
||
<p className="text-xs text-slate-400 mt-1">Orders within 24h will appear here</p>
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Sales vs Payroll Chart */}
|
||
<Card className="bg-white border-slate-200 shadow-sm">
|
||
<CardHeader className="border-b border-slate-100 pb-4">
|
||
<CardTitle className="text-base flex items-center gap-2">
|
||
<TrendingUp className="w-5 h-5 text-[#0A39DF]" />
|
||
Sales vs Payroll
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="p-6">
|
||
<ResponsiveContainer width="100%" height={280}>
|
||
<BarChart data={salesPayrollTrend}>
|
||
<CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" vertical={false} />
|
||
<XAxis
|
||
dataKey="month"
|
||
tick={{ fontSize: 12 }}
|
||
stroke="#94a3b8"
|
||
axisLine={false}
|
||
tickLine={false}
|
||
/>
|
||
<YAxis
|
||
tick={{ fontSize: 12 }}
|
||
stroke="#94a3b8"
|
||
axisLine={false}
|
||
tickLine={false}
|
||
tickFormatter={(value) => `${(value / 1000).toFixed(0)}k`}
|
||
/>
|
||
<Tooltip
|
||
contentStyle={{
|
||
fontSize: '12px',
|
||
backgroundColor: 'white',
|
||
border: '1px solid #e2e8f0',
|
||
borderRadius: '12px',
|
||
padding: '8px 12px'
|
||
}}
|
||
formatter={(value) => [`$${Math.round(value).toLocaleString()}`, '']}
|
||
/>
|
||
<Bar dataKey="payroll" fill="#1C323E" radius={[4, 4, 0, 0]} />
|
||
<Bar dataKey="sales" fill="#000000" radius={[4, 4, 0, 0]} />
|
||
</BarChart>
|
||
</ResponsiveContainer>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Client Analysis */}
|
||
<Card className="bg-white border-slate-200 shadow-sm">
|
||
<CardHeader className="border-b border-slate-100 pb-4">
|
||
<CardTitle className="text-base flex items-center gap-2">
|
||
<BarChart3 className="w-5 h-5 text-[#0A39DF]" />
|
||
Client Analysis
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="p-6">
|
||
{topClients.length > 0 ? (
|
||
<div className="space-y-5">
|
||
{topClients.map((client) => (
|
||
<div key={client.name} className="space-y-2">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<h4 className="font-bold text-[#1C323E]">{client.name}</h4>
|
||
<p className="text-xs text-slate-500">Fill</p>
|
||
</div>
|
||
<div className="text-right">
|
||
<p className="font-bold text-[#1C323E]">${(client.revenue / 1000).toFixed(0)}k</p>
|
||
<p className="text-xs text-slate-500">{Math.round(client.fillRate)}%</p>
|
||
</div>
|
||
</div>
|
||
<div className="w-full h-2 bg-slate-100 rounded-full overflow-hidden">
|
||
<div
|
||
className="h-full bg-[#1C323E] rounded-full transition-all duration-500"
|
||
style={{ width: `${client.fillRate}%` }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<div className="text-center py-8">
|
||
<BarChart3 className="w-10 h-10 mx-auto text-slate-300 mb-2" />
|
||
<p className="text-sm text-slate-500">No client data</p>
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
|
||
{/* Right Column (1 col) */}
|
||
<div className="space-y-6">
|
||
|
||
{/* Total Revenue Card - NEW DESIGN */}
|
||
<Card className="bg-gradient-to-br from-[#0A39DF] via-blue-700 to-[#1C323E] border-0 shadow-xl overflow-hidden">
|
||
<CardContent className="p-8 relative">
|
||
<div className="absolute top-4 right-4 flex gap-1.5">
|
||
<div className="w-2 h-2 bg-white/40 rounded-full"></div>
|
||
<div className="w-2 h-2 bg-white/40 rounded-full"></div>
|
||
<div className="w-2 h-2 bg-white rounded-full"></div>
|
||
</div>
|
||
<p className="text-white/80 text-sm mb-4 font-medium">Total Revenue</p>
|
||
<p className="text-6xl font-bold text-white mb-2">
|
||
${Math.round(totalRevenue / 1000)}k
|
||
</p>
|
||
<p className="text-white/70 text-sm">All time earnings</p>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Quick Actions - NEW DESIGN */}
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<Link to={createPageUrl("VendorOrders")}>
|
||
<Card className="bg-gradient-to-br from-[#0A39DF] to-blue-600 border-0 shadow-md hover:shadow-lg transition-all cursor-pointer group">
|
||
<CardContent className="p-6 text-center">
|
||
<div className="w-14 h-14 mx-auto mb-3 bg-white/20 rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform">
|
||
<Package className="w-7 h-7 text-white" />
|
||
</div>
|
||
<p className="text-white font-bold text-base mb-1">All Orders</p>
|
||
<p className="text-white/70 text-xs">View & manage</p>
|
||
</CardContent>
|
||
</Card>
|
||
</Link>
|
||
|
||
<Link to={createPageUrl("StaffDirectory")}>
|
||
<Card className="bg-white border-slate-200 shadow-sm hover:shadow-md transition-all cursor-pointer group">
|
||
<CardContent className="p-6 text-center">
|
||
<div className="w-14 h-14 mx-auto mb-3 bg-gradient-to-br from-blue-50 to-indigo-50 rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform">
|
||
<Users className="w-7 h-7 text-blue-600" />
|
||
</div>
|
||
<p className="text-[#1C323E] font-bold text-base mb-1">My Orders</p>
|
||
<p className="text-slate-500 text-xs">Manage staff</p>
|
||
</CardContent>
|
||
</Card>
|
||
</Link>
|
||
</div>
|
||
|
||
{/* Today's Metrics */}
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<Card className="bg-white border-slate-200 shadow-sm">
|
||
<CardContent className="p-5">
|
||
<div className="flex items-center justify-between mb-3">
|
||
<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>
|
||
<ArrowRight className="w-4 h-4 text-slate-300" />
|
||
</div>
|
||
<p className="text-3xl font-bold text-[#1C323E] mb-1">{todayOrders.length}</p>
|
||
<p className="text-sm text-slate-600">Orders Today</p>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<Card className="bg-white border-slate-200 shadow-sm">
|
||
<CardContent className="p-5">
|
||
<div className="flex items-center justify-between mb-3">
|
||
<div className="w-10 h-10 bg-purple-50 rounded-lg flex items-center justify-center">
|
||
<Package className="w-5 h-5 text-purple-600" />
|
||
</div>
|
||
<Badge className="bg-green-100 text-green-700 text-xs font-bold">Active</Badge>
|
||
</div>
|
||
<p className="text-3xl font-bold text-[#1C323E] mb-1">{activeOrders.length}</p>
|
||
<p className="text-sm text-slate-600">In Progress</p>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
|
||
{/* Staff Assigned Today */}
|
||
<Card className="bg-white border-slate-200 shadow-sm">
|
||
<CardContent className="p-5">
|
||
<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">
|
||
<Users className="w-5 h-5 text-amber-600" />
|
||
</div>
|
||
<Badge className="bg-amber-100 text-amber-700 text-xs font-bold">Today</Badge>
|
||
</div>
|
||
<p className="text-4xl font-bold text-[#1C323E] mb-1">{staffAssignedToday}</p>
|
||
<p className="text-sm text-slate-600">Staff Assigned</p>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Top Clients - NEW DESIGN */}
|
||
<Card className="bg-white border-slate-200 shadow-sm">
|
||
<CardHeader className="pb-3">
|
||
<CardTitle className="text-sm flex items-center gap-2">
|
||
<Star className="w-4 h-4 text-amber-500" />
|
||
Top Clients
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="p-4">
|
||
{topClients.slice(0, 3).length > 0 ? (
|
||
<div className="space-y-3">
|
||
{topClients.slice(0, 3).map((client) => (
|
||
<div key={client.name} className="flex items-center justify-between p-3 rounded-lg bg-slate-50 border border-slate-200">
|
||
<div>
|
||
<p className="font-bold text-sm text-[#1C323E]">{client.name}</p>
|
||
<p className="text-xs text-slate-500">+{client.orders}%</p>
|
||
</div>
|
||
<p className="font-bold text-[#1C323E]">${(client.revenue / 1000).toFixed(0)}k</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<div className="text-center py-6">
|
||
<Star className="w-8 h-8 mx-auto text-slate-300 mb-2" />
|
||
<p className="text-xs text-slate-500">No client data</p>
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Top Performers - NEW DESIGN */}
|
||
<Card className="bg-white border-slate-200 shadow-sm">
|
||
<CardHeader className="pb-3">
|
||
<CardTitle className="text-sm flex items-center gap-2">
|
||
<Award className="w-4 h-4 text-blue-500" />
|
||
Top Performers
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="p-4">
|
||
{topPerformers.length > 0 ? (
|
||
<div className="space-y-3">
|
||
{topPerformers.map((member) => (
|
||
<div key={member.id} className="flex items-center justify-between p-3 rounded-lg bg-slate-50 border border-slate-200">
|
||
<div>
|
||
<p className="font-bold text-sm text-[#1C323E]">
|
||
{member.employee_name} - {member.position || "Staff"}
|
||
</p>
|
||
<p className="text-xs text-slate-500">{member.shifts} shifts</p>
|
||
</div>
|
||
<div className="flex items-center gap-1">
|
||
<span className="text-sm font-bold text-[#1C323E]">{(member.rating || 0).toFixed(1)}</span>
|
||
<Star className="w-3 h-3 text-amber-500 fill-amber-500" />
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<div className="text-center py-6">
|
||
<Award className="w-8 h-8 mx-auto text-slate-300 mb-2" />
|
||
<p className="text-xs text-slate-500">No staff data</p>
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Gold Vendors - NEW SECTION */}
|
||
<Card className="bg-white border-slate-200 shadow-sm">
|
||
<CardHeader className="pb-3">
|
||
<CardTitle className="text-sm flex items-center gap-2">
|
||
<Award className="w-4 h-4 text-amber-500" />
|
||
Gold Vendors
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="p-4">
|
||
<div className="space-y-3">
|
||
<div className="flex items-center justify-between p-3 rounded-lg bg-gradient-to-r from-amber-50 to-yellow-50 border border-amber-200">
|
||
<div>
|
||
<p className="font-bold text-sm text-[#1C323E]">Legendary Staffing</p>
|
||
<p className="text-xs text-slate-500">Score</p>
|
||
</div>
|
||
<p className="text-2xl font-bold text-amber-600">98</p>
|
||
</div>
|
||
<div className="flex items-center justify-between p-3 rounded-lg bg-gradient-to-r from-amber-50 to-yellow-50 border border-amber-200">
|
||
<div>
|
||
<p className="font-bold text-sm text-[#1C323E]">Epic Workforce</p>
|
||
<p className="text-xs text-slate-500">Score</p>
|
||
</div>
|
||
<p className="text-2xl font-bold text-amber-600">96</p>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
} |