From 02374b6b0550ab26f2e3eda1183defa2965ba0fd Mon Sep 17 00:00:00 2001 From: dhinesh-m24 Date: Thu, 12 Feb 2026 12:46:57 +0530 Subject: [PATCH] feat: Implement vendor dashboard --- .../features/dashboard/VendorDashboard.tsx | 313 +++++++++++++++++- .../workforce/directory/StaffList.tsx | 31 +- 2 files changed, 335 insertions(+), 9 deletions(-) diff --git a/apps/web/src/features/dashboard/VendorDashboard.tsx b/apps/web/src/features/dashboard/VendorDashboard.tsx index 3e78459a..b7f34167 100644 --- a/apps/web/src/features/dashboard/VendorDashboard.tsx +++ b/apps/web/src/features/dashboard/VendorDashboard.tsx @@ -1,9 +1,310 @@ - +import { useState, useMemo, useCallback } from 'react'; +import { useSelector } from 'react-redux'; +import { useNavigate } from 'react-router-dom'; +import { + useGetVendorByUserId, + useGetOrdersByVendorId, + useListShiftsForDailyOpsByVendor, + useListApplicationsForDailyOps, + useListInvoicesForSpendByVendor, + useListShiftsForPerformanceByVendor +} from '@/dataconnect-generated/react'; +import { dataConnect } from '@/features/auth/firebase'; +import type { RootState } from '@/store/store'; +import DashboardLayout from '@/features/layouts/DashboardLayout'; +import { Card, CardContent, CardHeader, CardTitle } from '@/common/components/ui/card'; +import { Button } from '@/common/components/ui/button'; +import { Badge } from '@/common/components/ui/badge'; +import { Calendar } from '@/common/components/ui/calendar'; +import { + ClipboardList, + Users, + DollarSign, + BarChart3, + Clock, + Calendar as CalendarIcon, + UserCheck +} from 'lucide-react'; +import { format, startOfMonth, endOfMonth } from 'date-fns'; +import { motion } from 'framer-motion'; +import type { Variants } from 'framer-motion'; const VendorDashboard = () => { - return ( -
VendorDashboard
- ) -} + const navigate = useNavigate(); + const { user } = useSelector((state: RootState) => state.auth); + const [selectedDate, setSelectedDate] = useState(new Date()); -export default VendorDashboard \ No newline at end of file + // 1. Get vendor for the logged in user + const { data: vendorData, isLoading: isLoadingVendor } = useGetVendorByUserId(dataConnect, { + userId: user?.uid || "" + }, { + enabled: !!user?.uid + }); + + // FIXED: Wrap in useMemo to prevent infinite re-renders + const vendor = useMemo(() => vendorData?.vendors[0], [vendorData]); + const vendorId = useMemo(() => vendor?.id, [vendor]); + + // 2. Fetch Dashboard Data + // FIXED: Use useMemo for date calculations to prevent recalculation on every render + const dateRange = useMemo(() => { + const today = new Date(); + return { + today, + monthStart: startOfMonth(today), + monthEnd: endOfMonth(today) + }; + }, []); + + const { data: ordersData } = useGetOrdersByVendorId(dataConnect, { + vendorId: vendorId || "" + }, { + enabled: !!vendorId + }); + + const { data: dailyShiftsData } = useListShiftsForDailyOpsByVendor(dataConnect, { + vendorId: vendorId || "", + date: dateRange.today.toISOString() + }, { + enabled: !!vendorId + }); + + const shiftIds = useMemo(() => + dailyShiftsData?.shifts.map(s => s.id) || [], + [dailyShiftsData] + ); + + const { data: dailyAppsData } = useListApplicationsForDailyOps(dataConnect, { + shiftIds + }, { + enabled: shiftIds.length > 0 + }); + + const { data: revenueData } = useListInvoicesForSpendByVendor(dataConnect, { + vendorId: vendorId || "", + startDate: dateRange.monthStart.toISOString(), + endDate: dateRange.monthEnd.toISOString() + }, { + enabled: !!vendorId + }); + + const { data: performanceData } = useListShiftsForPerformanceByVendor(dataConnect, { + vendorId: vendorId || "", + startDate: dateRange.monthStart.toISOString(), + endDate: dateRange.monthEnd.toISOString() + }, { + enabled: !!vendorId + }); + + // 3. KPI Calculations + const stats = useMemo(() => { + const activeOrders = (ordersData?.orders || []).filter(o => + o.status !== 'COMPLETED' && o.status !== 'CANCELLED' + ).length; + + const staffOnShift = (dailyAppsData?.applications || []).filter(a => + a.status === 'CHECKED_IN' + ).length; + + const monthlyRevenue = (revenueData?.invoices || []).reduce((sum, inv) => + sum + (inv.amount || 0), 0 + ); + + const shifts = performanceData?.shifts || []; + const totalNeeded = shifts.reduce((sum, s) => sum + (s.workersNeeded || 0), 0); + const totalFilled = shifts.reduce((sum, s) => sum + (s.filled || 0), 0); + const fillRate = totalNeeded > 0 ? Math.round((totalFilled / totalNeeded) * 100) : 0; + + return { + activeOrders, + staffOnShift, + monthlyRevenue, + fillRate + }; + }, [ordersData, dailyAppsData, revenueData, performanceData]); + + // FIXED: Stable navigation handlers with useCallback + const handleNavigateToOrders = useCallback(() => { + console.log("Navigating to vendor orders page"); + navigate('/orders/vendor'); + }, [navigate]); + + const handleNavigateToStaff = useCallback(() => { + navigate('/staff'); + }, [navigate]); + + // Animation variants + const containerVariants: Variants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { + staggerChildren: 0.1 + } + } + }; + + const itemVariants: Variants = { + hidden: { opacity: 0, y: 20 }, + visible: { + opacity: 1, + y: 0, + transition: { + duration: 0.5, + ease: "easeOut" + } + } + }; + + if (isLoadingVendor) { + return
Loading dashboard...
; + } + + return ( + + + + + } + > + + {/* Stats Grid */} + + + + Active Orders +
+ +
+
+ +
{stats.activeOrders}
+

Currently in progress

+
+
+ + + + Staff on Shift +
+ +
+
+ +
{stats.staffOnShift}
+

Working right now

+
+
+ + + + Monthly Revenue +
+ +
+
+ +
${stats.monthlyRevenue.toLocaleString()}
+

Total for {format(dateRange.today, 'MMMM')}

+
+
+ + + + Shift Fill Rate +
+ +
+
+ +
{stats.fillRate}%
+
+ +
+
+
+
+ + {/* Schedule Overview */} + + + + Daily Operations + + +
+
+ +
+
+

+ {selectedDate ? format(selectedDate, 'EEEE, MMMM d, yyyy') : 'Select a date'} +

+
+ {dailyShiftsData?.shifts.length === 0 ? ( +
+ +

No shifts for this date

+
+ ) : ( + dailyShiftsData?.shifts.map(shift => ( +
+
+
+

{shift.title}

+
+ + {shift.startTime ? format(new Date(shift.startTime), 'p') : ''} +
+
+ + {shift.status} + +
+
+ )) + )} +
+
+
+
+
+
+
+
+ ); +}; + +export default VendorDashboard; \ No newline at end of file diff --git a/apps/web/src/features/workforce/directory/StaffList.tsx b/apps/web/src/features/workforce/directory/StaffList.tsx index e039a7f7..32b4d788 100644 --- a/apps/web/src/features/workforce/directory/StaffList.tsx +++ b/apps/web/src/features/workforce/directory/StaffList.tsx @@ -1,5 +1,8 @@ import { useState, useMemo} from "react"; import { Link } from "react-router-dom"; +import { useSelector } from "react-redux"; +import type { RootState } from "@/store/store"; +import { useToast } from "@/common/components/ui/use-toast"; import { Button } from "../../../common/components/ui/button"; import { Card, CardContent } from "../../../common/components/ui/card"; import { Badge } from "../../../common/components/ui/badge"; @@ -30,14 +33,29 @@ function StaffActiveStatus({ staffId }: { staffId: string }) { } export default function StaffList() { + const { toast } = useToast(); const [searchTerm, setSearchTerm] = useState(""); const [statusFilter, setStatusFilter] = useState("all"); const [skillsFilter, setSkillsFilter] = useState([]); const [ratingRange, setRatingRange] = useState<[number, number]>([0, 5]); const [currentPage, setCurrentPage] = useState(1); + const user = useSelector((state: RootState) => state.auth.user); + const isAdmin = user?.userRole === "admin" || user?.userRole === "ADMIN"; + const { data: staffData, isLoading } = useListStaff(dataConnect); + const handleRestrictedAction = (e: React.MouseEvent) => { + if (!isAdmin) { + e.preventDefault(); + toast({ + title: "Access Restricted", + description: "Only administrators can perform this action.", + variant: "destructive" + }); + } + }; + const staff = useMemo(() => { return staffData?.staffs || []; }, [staffData]); @@ -103,8 +121,11 @@ export default function StaffList() { title="Staff Directory" subtitle={`${filteredStaff.length} staff members`} actions={ - - @@ -289,7 +310,11 @@ export default function StaffList() { className="hover:bg-primary/5 transition-colors group" > - + {member.fullName || 'N/A'}