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.
This commit is contained in:
740
frontend-web/src/pages/VendorDashboard.jsx
Normal file
740
frontend-web/src/pages/VendorDashboard.jsx
Normal file
@@ -0,0 +1,740 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user