feat: Initial commit of KROW Workforce Web client (Base44 export)

This commit is contained in:
bwnyasse
2025-11-11 06:08:01 -05:00
commit e571193362
173 changed files with 50898 additions and 0 deletions

View 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>
);
}