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 (
+
+ }>
+ View Orders
+
+ }>
+ Manage Staff
+
+
+ }
+ >
+
+ {/* 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={
-
- }>
+
+ }
+ className={!isAdmin ? "opacity-50 cursor-not-allowed hover:opacity-50" : ""}
+ >
Add New Staff
@@ -289,7 +310,11 @@ export default function StaffList() {
className="hover:bg-primary/5 transition-colors group"
>
-
+
{member.fullName || 'N/A'}
|