feat: Initial commit of KROW Workforce Web client (Base44 export)
This commit is contained in:
424
src/pages/ClientDashboard.jsx
Normal file
424
src/pages/ClientDashboard.jsx
Normal file
@@ -0,0 +1,424 @@
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { base44 } from "@/api/base44Client";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Link } from "react-router-dom";
|
||||
import { createPageUrl } from "@/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Calendar, Plus, Clock, CheckCircle, DollarSign, FileText, MessageSquare, RefreshCw, Zap, TrendingUp, Star, ArrowRight, Users, CloudOff, MapPin } from "lucide-react";
|
||||
import { format, parseISO } from "date-fns";
|
||||
import QuickReorderModal from "../components/events/QuickReorderModal";
|
||||
|
||||
export default function ClientDashboard() {
|
||||
const [reorderModalOpen, setReorderModalOpen] = useState(false);
|
||||
const [selectedEvent, setSelectedEvent] = useState(null);
|
||||
|
||||
const { data: user } = useQuery({
|
||||
queryKey: ['current-user'],
|
||||
queryFn: () => base44.auth.me(),
|
||||
});
|
||||
|
||||
const { data: events } = useQuery({
|
||||
queryKey: ['client-events'],
|
||||
queryFn: async () => {
|
||||
const allEvents = await base44.entities.Event.list('-date');
|
||||
const clientEvents = allEvents.filter(e =>
|
||||
e.client_email === user?.email ||
|
||||
e.business_name === user?.company_name ||
|
||||
e.created_by === user?.email
|
||||
);
|
||||
|
||||
if (clientEvents.length === 0) {
|
||||
return allEvents.filter(e => e.status === "Completed");
|
||||
}
|
||||
|
||||
return clientEvents;
|
||||
},
|
||||
initialData: [],
|
||||
enabled: !!user
|
||||
});
|
||||
|
||||
const pendingOrders = events.filter(e => e.status === "Pending" || e.status === "Draft").length;
|
||||
const activeOrders = events.filter(e => e.status === "Active" || e.status === "Confirmed").length;
|
||||
const completedOrders = events.filter(e => e.status === "Completed").length;
|
||||
|
||||
const upcomingEvents = events
|
||||
.filter(e => new Date(e.date) > new Date() && e.status !== "Canceled")
|
||||
.slice(0, 5);
|
||||
|
||||
const pastOrders = events.filter(e => e.status === "Completed");
|
||||
|
||||
const orderFrequency = pastOrders.reduce((acc, event) => {
|
||||
const key = event.event_name;
|
||||
if (!acc[key]) {
|
||||
acc[key] = { event, count: 0, lastOrdered: event.date };
|
||||
}
|
||||
acc[key].count++;
|
||||
if (new Date(event.date) > new Date(acc[key].lastOrdered)) {
|
||||
acc[key].lastOrdered = event.date;
|
||||
acc[key].event = event;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const frequentOrders = Object.values(orderFrequency)
|
||||
.sort((a, b) => {
|
||||
if (b.count !== a.count) return b.count - a.count;
|
||||
return new Date(b.lastOrdered) - new Date(a.lastOrdered);
|
||||
})
|
||||
.slice(0, 3);
|
||||
|
||||
const handleQuickReorder = (event) => {
|
||||
setSelectedEvent(event);
|
||||
setReorderModalOpen(true);
|
||||
};
|
||||
|
||||
const getMostLikedRank = (index) => {
|
||||
if (index === 0) return { text: "#1 Most Ordered", color: "bg-gradient-to-r from-amber-500 to-orange-500", icon: "🏆" };
|
||||
if (index === 1) return { text: "#2 Most Popular", color: "bg-gradient-to-r from-blue-500 to-indigo-500", icon: "⭐" };
|
||||
if (index === 2) return { text: "#3 Top Choice", color: "bg-gradient-to-r from-purple-500 to-pink-500", icon: "💎" };
|
||||
return null;
|
||||
};
|
||||
|
||||
// Calculate time savings (assuming each manual order takes 15 minutes)
|
||||
const totalReorders = frequentOrders.reduce((sum, item) => sum + item.count, 0);
|
||||
const timeSavedMinutes = totalReorders * 15; // 15 minutes per order
|
||||
const timeSavedHours = Math.floor(timeSavedMinutes / 60);
|
||||
const remainingMinutes = timeSavedMinutes % 60;
|
||||
|
||||
const handleRefresh = () => {
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-slate-50">
|
||||
<div className="max-w-[1600px] mx-auto p-4 md:p-8">
|
||||
|
||||
{/* Unpublished Changes */}
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
className="flex items-center gap-2 mb-4 text-slate-500 hover:text-[#0A39DF] hover:bg-blue-50 px-3 py-2 rounded-lg transition-all group"
|
||||
>
|
||||
<CloudOff className="w-5 h-5 group-hover:animate-pulse" />
|
||||
<span className="text-sm font-medium">Unpublished changes</span>
|
||||
<RefreshCw className="w-4 h-4 opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
</button>
|
||||
|
||||
{/* Main Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl md:text-4xl font-bold bg-gradient-to-r from-[#1C323E] to-[#0A39DF] bg-clip-text text-transparent mb-2">
|
||||
Welcome back, {user?.full_name || user?.company_name || 'User'}
|
||||
</h1>
|
||||
<p className="text-lg text-slate-600">Streamline your workforce management</p>
|
||||
</div>
|
||||
|
||||
{/* ORDER IT AGAIN - DOORDASH STYLE */}
|
||||
{frequentOrders.length > 0 && (
|
||||
<div className="mb-12">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-[#1C323E] mb-1">Order it again</h2>
|
||||
<p className="text-slate-600">Your go-to orders • Click to reorder instantly</p>
|
||||
</div>
|
||||
<div className="bg-gradient-to-r from-green-50 to-emerald-50 px-6 py-3 rounded-xl border border-green-200">
|
||||
<p className="text-xs text-green-700 font-semibold uppercase mb-1">⏱️ Time Saved</p>
|
||||
<p className="text-2xl font-bold text-green-600">
|
||||
{timeSavedHours > 0 ? `${timeSavedHours}h ${remainingMinutes}m` : `${timeSavedMinutes}m`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scrollable Cards */}
|
||||
<div className="relative">
|
||||
{/* Fade Edges */}
|
||||
<div className="absolute left-0 top-0 bottom-0 w-8 bg-gradient-to-r from-slate-50 via-blue-50/30 to-transparent z-10 pointer-events-none"></div>
|
||||
<div className="absolute right-0 top-0 bottom-0 w-8 bg-gradient-to-l from-slate-50 via-blue-50/30 to-transparent z-10 pointer-events-none"></div>
|
||||
|
||||
<div className="flex gap-4 overflow-x-auto pb-4 snap-x snap-mandatory scrollbar-hide px-1">
|
||||
{frequentOrders.map((item, index) => {
|
||||
const { event, count, lastOrdered } = item;
|
||||
const rank = getMostLikedRank(index);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={event.id}
|
||||
className="flex-shrink-0 w-[320px] snap-start group cursor-pointer"
|
||||
onClick={() => handleQuickReorder(event)}
|
||||
>
|
||||
<div className="relative bg-white rounded-2xl border-2 border-slate-200 hover:border-[#0A39DF] hover:shadow-2xl hover:scale-105 transition-all duration-300 overflow-hidden">
|
||||
{/* Rank Badge */}
|
||||
{rank && (
|
||||
<div className="absolute top-3 left-3 z-10">
|
||||
<div className={`${rank.color} text-white text-xs font-bold px-3 py-1.5 rounded-full shadow-lg flex items-center gap-1.5`}>
|
||||
<span>{rank.icon}</span>
|
||||
<span>#{index + 1}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Reorder Button - Top Right + Icon */}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleQuickReorder(event);
|
||||
}}
|
||||
className="absolute top-3 right-3 w-10 h-10 bg-white rounded-full shadow-lg border-2 border-slate-200 flex items-center justify-center hover:bg-[#0A39DF] hover:border-[#0A39DF] hover:scale-110 transition-all z-10 group/btn"
|
||||
>
|
||||
<Plus className="w-5 h-5 text-slate-700 group-hover/btn:text-white transition-colors" />
|
||||
</button>
|
||||
|
||||
{/* Hover Overlay */}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-[#0A39DF]/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-[1]" />
|
||||
|
||||
{/* Card Content */}
|
||||
<div className="p-5 pt-14 relative z-[2]">
|
||||
{/* Event Name */}
|
||||
<h3 className="font-bold text-lg text-[#1C323E] mb-3 line-clamp-2 leading-tight group-hover:text-[#0A39DF] transition-colors">
|
||||
{event.event_name}
|
||||
</h3>
|
||||
|
||||
{/* Manager & Hub Info */}
|
||||
<div className="space-y-2 mb-3">
|
||||
{event.manager_name && (
|
||||
<div className="flex items-center gap-2 text-sm text-slate-600">
|
||||
<div className="w-6 h-6 bg-purple-100 rounded-full flex items-center justify-center flex-shrink-0">
|
||||
<Users className="w-3.5 h-3.5 text-purple-600" />
|
||||
</div>
|
||||
<span className="font-medium">{event.manager_name}</span>
|
||||
</div>
|
||||
)}
|
||||
{event.hub && (
|
||||
<div className="flex items-center gap-2 text-sm text-slate-600">
|
||||
<div className="w-6 h-6 bg-blue-100 rounded-full flex items-center justify-center flex-shrink-0">
|
||||
🍽️
|
||||
</div>
|
||||
<span className="font-medium">{event.hub}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Stats Row */}
|
||||
<div className="flex items-center gap-3 text-sm text-slate-600 mb-4 pb-4 border-b border-slate-100">
|
||||
<span className="flex items-center gap-1 bg-slate-50 px-2 py-1 rounded-lg">
|
||||
<RefreshCw className="w-3.5 h-3.5 text-[#0A39DF]" />
|
||||
<span className="font-semibold text-[#1C323E]">{count}x</span>
|
||||
</span>
|
||||
<span className="text-xs text-slate-400">•</span>
|
||||
<span className="text-xs font-medium">Last: {format(parseISO(lastOrdered), "MMM d")}</span>
|
||||
</div>
|
||||
|
||||
{/* Quick Info */}
|
||||
<div className="space-y-2 text-sm">
|
||||
{event.event_location && (
|
||||
<div className="flex items-center gap-2 text-slate-600">
|
||||
<MapPin className="w-4 h-4 text-slate-400 flex-shrink-0" />
|
||||
<span className="truncate">{event.event_location}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Users className="w-4 h-4 text-blue-600" />
|
||||
<span className="font-semibold text-[#1C323E]">{event.requested || 1}</span>
|
||||
<span className="text-xs text-slate-500">staff</span>
|
||||
</div>
|
||||
{event.total && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<DollarSign className="w-4 h-4 text-emerald-600" />
|
||||
<span className="font-semibold text-[#1C323E]">${event.total.toLocaleString()}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Reorder Button at Bottom */}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleQuickReorder(event);
|
||||
}}
|
||||
className="w-full mt-4 bg-gradient-to-r from-[#0A39DF] to-[#1C323E] hover:from-[#0829B0] hover:to-[#0F1D26] text-white font-semibold py-3 rounded-xl flex items-center justify-center gap-2 shadow-md hover:shadow-xl transition-all group/reorder"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4 group-hover/reorder:rotate-180 transition-transform duration-500" />
|
||||
Reorder Now
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Bottom Accent Stripe */}
|
||||
<div className="h-1.5 bg-gradient-to-r from-[#0A39DF] via-purple-500 to-[#1C323E] group-hover:h-2 transition-all"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Scroll Hint */}
|
||||
<div className="text-center mt-4">
|
||||
<p className="text-xs text-slate-400 flex items-center justify-center gap-2">
|
||||
<span>Swipe to see more</span>
|
||||
<ArrowRight className="w-3 h-3 animate-pulse" />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Quick Actions - More Professional */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||
<Link to={createPageUrl("CreateEvent")} className="block group">
|
||||
<Card className="border-2 border-[#0A39DF]/20 hover:border-[#0A39DF] hover:shadow-xl transition-all h-full bg-gradient-to-br from-white to-blue-50/50">
|
||||
<CardContent className="p-8 text-center">
|
||||
<div className="w-20 h-20 mx-auto mb-5 bg-gradient-to-br from-[#0A39DF] to-[#1C323E] rounded-2xl flex items-center justify-center shadow-xl group-hover:scale-110 transition-transform">
|
||||
<Plus className="w-10 h-10 text-white" />
|
||||
</div>
|
||||
<h3 className="font-bold text-[#1C323E] mb-2 text-xl">New Order</h3>
|
||||
<p className="text-sm text-slate-500">Request staff for your event</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
|
||||
<Link to={createPageUrl("ClientOrders")} className="block group">
|
||||
<Card className="border-2 border-slate-200 hover:border-purple-500 hover:shadow-xl transition-all h-full bg-gradient-to-br from-white to-purple-50/50">
|
||||
<CardContent className="p-8 text-center">
|
||||
<div className="w-20 h-20 mx-auto mb-5 bg-gradient-to-br from-purple-500 to-purple-700 rounded-2xl flex items-center justify-center shadow-xl group-hover:scale-110 transition-transform">
|
||||
<Calendar className="w-10 h-10 text-white" />
|
||||
</div>
|
||||
<h3 className="font-bold text-[#1C323E] mb-2 text-xl">My Orders</h3>
|
||||
<p className="text-sm text-slate-500">View all your orders</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
|
||||
<Link to={createPageUrl("Messages")} className="block group">
|
||||
<Card className="border-2 border-slate-200 hover:border-emerald-500 hover:shadow-xl transition-all h-full bg-gradient-to-br from-white to-emerald-50/50">
|
||||
<CardContent className="p-8 text-center">
|
||||
<div className="w-20 h-20 mx-auto mb-5 bg-gradient-to-br from-emerald-500 to-emerald-700 rounded-2xl flex items-center justify-center shadow-xl group-hover:scale-110 transition-transform">
|
||||
<MessageSquare className="w-10 h-10 text-white" />
|
||||
</div>
|
||||
<h3 className="font-bold text-[#1C323E] mb-2 text-xl">Messages</h3>
|
||||
<p className="text-sm text-slate-500">Contact support</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards - More Professional */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||
<Card className="border-slate-200 shadow-lg hover:shadow-xl transition-shadow bg-gradient-to-br from-white to-amber-50/50">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<Clock className="w-10 h-10 text-amber-600" />
|
||||
<Badge className="bg-amber-100 text-amber-700 text-lg px-3 py-1">{pendingOrders}</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-slate-500 mb-1 font-medium">Pending Orders</p>
|
||||
<p className="text-4xl font-bold text-[#1C323E]">{pendingOrders}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-slate-200 shadow-lg hover:shadow-xl transition-shadow bg-gradient-to-br from-white to-blue-50/50">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<Calendar className="w-10 h-10 text-[#0A39DF]" />
|
||||
<Badge className="bg-blue-100 text-blue-700 text-lg px-3 py-1">{activeOrders}</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-slate-500 mb-1 font-medium">Active Orders</p>
|
||||
<p className="text-4xl font-bold text-[#1C323E]">{activeOrders}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-slate-200 shadow-lg hover:shadow-xl transition-shadow bg-gradient-to-br from-white to-emerald-50/50">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<CheckCircle className="w-10 h-10 text-emerald-600" />
|
||||
<Badge className="bg-emerald-100 text-emerald-700 text-lg px-3 py-1">{completedOrders}</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-slate-500 mb-1 font-medium">Completed</p>
|
||||
<p className="text-4xl font-bold text-[#1C323E]">{completedOrders}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-slate-200 shadow-lg hover:shadow-xl transition-shadow bg-gradient-to-br from-white to-purple-50/50">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<FileText className="w-10 h-10 text-purple-600" />
|
||||
<Badge className="bg-purple-100 text-purple-700 text-lg px-3 py-1">0</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-slate-500 mb-1 font-medium">Unpaid Invoices</p>
|
||||
<p className="text-4xl font-bold text-[#1C323E]">{0}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Upcoming Events - More Professional */}
|
||||
<Card className="border-slate-200 shadow-lg">
|
||||
<CardHeader className="bg-gradient-to-br from-slate-50 to-white border-b border-slate-200">
|
||||
<CardTitle className="flex items-center gap-3 text-xl">
|
||||
<Calendar className="w-6 h-6 text-[#0A39DF]" />
|
||||
Upcoming Events
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-6">
|
||||
{upcomingEvents.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{upcomingEvents.map((event) => (
|
||||
<div key={event.id} className="flex items-center justify-between p-5 bg-gradient-to-r from-slate-50 to-blue-50/50 rounded-xl hover:shadow-md transition-all border border-slate-200 hover:border-[#0A39DF]/30">
|
||||
<div className="flex-1">
|
||||
<h4 className="font-bold text-[#1C323E] text-lg mb-1">{event.event_name}</h4>
|
||||
<p className="text-sm text-slate-600 flex items-center gap-2">
|
||||
<Calendar className="w-4 h-4" />
|
||||
{format(new Date(event.date), "MMMM dd, yyyy")}
|
||||
<span className="text-slate-400">•</span>
|
||||
<Users className="w-4 h-4" />
|
||||
{event.requested || 0} staff requested
|
||||
</p>
|
||||
</div>
|
||||
<Badge className={
|
||||
event.status === "Confirmed" ? "bg-green-100 text-green-700 text-sm px-4 py-2" :
|
||||
event.status === "Active" ? "bg-blue-100 text-blue-700 text-sm px-4 py-2" :
|
||||
"bg-yellow-100 text-yellow-700 text-sm px-4 py-2"
|
||||
}>
|
||||
{event.status}
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-16">
|
||||
<Calendar className="w-20 h-20 mx-auto text-slate-300 mb-4" />
|
||||
<p className="text-slate-500 mb-6 text-lg">No upcoming events</p>
|
||||
<Link to={createPageUrl("CreateEvent")}>
|
||||
<Button className="bg-gradient-to-r from-[#0A39DF] to-[#1C323E] hover:from-[#0829B0] hover:to-[#12242A] text-lg px-8 py-6 shadow-xl">
|
||||
<Plus className="w-5 h-5 mr-2" />
|
||||
Create Your First Order
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Quick Reorder Modal */}
|
||||
{selectedEvent && (
|
||||
<QuickReorderModal
|
||||
event={selectedEvent}
|
||||
open={reorderModalOpen}
|
||||
onOpenChange={setReorderModalOpen}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Custom Scrollbar Styles */}
|
||||
<style jsx>{`
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.scrollbar-hide {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user