feat(Makefile): install frontend dependencies on dev command

feat(Makefile): patch Layout.jsx queryKey for local development
feat(frontend-web): mock base44 client for local development with role switching
feat(frontend-web): add event assignment modal with conflict detection and bulk assign
feat(frontend-web): add client dashboard with key metrics and quick actions
feat(frontend-web): add layout component with role-based navigation
feat(frontend-web): update various pages to use "@/components" alias
feat(frontend-web): update create event page with ai assistant toggle
feat(frontend-web): update dashboard page with new components
feat(frontend-web): update events page with quick assign popover
feat(frontend-web): update invite vendor page with hover card
feat(frontend-web): update messages page with conversation list and message thread
feat(frontend-web): update operator dashboard page with new components
feat(frontend-web): update partner management page with new components
feat(frontend-web): update permissions page with new components
feat(frontend-web): update procurement dashboard page with new components
feat(frontend-web): update smart vendor onboarding page with new components
feat(frontend-web): update staff directory page with new components
feat(frontend-web): update teams page with new components
feat(frontend-web): update user management page with new components
feat(frontend-web): update vendor compliance page with new components
feat(frontend-web): update main.jsx to include react query provider

feat: add vendor marketplace page
feat: add global import fix to prepare-export script
feat: add patch-layout-query-key script to fix query key
feat: update patch-base44-client script to use a more robust method
This commit is contained in:
bwnyasse
2025-11-13 14:56:31 -05:00
parent f449272ef0
commit 80cd49deb5
49 changed files with 2937 additions and 1508 deletions

View File

@@ -19,7 +19,7 @@ import {
Filter
} from "lucide-react";
import { formatDistanceToNow } from "date-fns";
import PageHeader from "../components/common/PageHeader";
import PageHeader from "@/components/common/PageHeader";
const iconMap = {
calendar: Calendar,

View File

@@ -9,7 +9,7 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Building2, ArrowLeft, Check } from "lucide-react";
import PageHeader from "../components/common/PageHeader";
import PageHeader from "@/components/common/PageHeader";
import { useToast } from "@/components/ui/use-toast";
export default function AddEnterprise() {

View File

@@ -10,7 +10,7 @@ import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { Briefcase, ArrowLeft, Check, Plus, X } from "lucide-react";
import PageHeader from "../components/common/PageHeader";
import PageHeader from "@/components/common/PageHeader";
import { useToast } from "@/components/ui/use-toast";
export default function AddPartner() {

View File

@@ -9,7 +9,7 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { MapPin, ArrowLeft, Check } from "lucide-react";
import PageHeader from "../components/common/PageHeader";
import PageHeader from "@/components/common/PageHeader";
import { useToast } from "@/components/ui/use-toast";
export default function AddSector() {

View File

@@ -5,7 +5,7 @@ import { useNavigate } from "react-router-dom";
import { createPageUrl } from "@/utils";
import { Button } from "@/components/ui/button";
import { ArrowLeft } from "lucide-react";
import StaffForm from "../components/staff/StaffForm";
import StaffForm from "@/components/staff/StaffForm";
export default function AddStaff() {
const navigate = useNavigate();

View File

@@ -10,13 +10,13 @@ import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Plus, Building2, Mail, Phone, MapPin, Search, Eye, Trash2, ChevronDown, ChevronRight } from "lucide-react";
import { Link, useNavigate } from "react-router-dom";
import { createPageUrl } from "@/utils";
import PageHeader from "../components/common/PageHeader";
import PageHeader from "@/components/common/PageHeader";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import CreateBusinessModal from "../components/business/CreateBusinessModal";
import CreateBusinessModal from "@/components/business/CreateBusinessModal";
export default function Business() {
const navigate = useNavigate();

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,7 @@ import { useNavigate } from "react-router-dom";
import { createPageUrl } from "@/utils";
import { format, addDays } from "date-fns";
import { useToast } from "@/components/ui/use-toast";
import QuickReorderModal from "../components/events/QuickReorderModal";
import QuickReorderModal from "@/components/events/QuickReorderModal";
export default function ClientOrders() {
const navigate = useNavigate();

View File

@@ -3,8 +3,8 @@ import { base44 } from "@/api/base44Client";
import { useMutation, useQueryClient, useQuery } from "@tanstack/react-query";
import { useNavigate } from "react-router-dom";
import { createPageUrl } from "@/utils";
import EventFormWizard from "../components/events/EventFormWizard";
import AIOrderAssistant from "../components/events/AIOrderAssistant";
import EventFormWizard from "@/components/events/EventFormWizard";
import AIOrderAssistant from "@/components/events/AIOrderAssistant";
import { useToast } from "@/components/ui/use-toast";
import { Button } from "@/components/ui/button";
import { Sparkles, FileText, X } from "lucide-react";

View File

@@ -7,10 +7,10 @@ import { createPageUrl } from "@/utils";
import { Button } from "@/components/ui/button";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Users, Building2, UserPlus, TrendingUp, MapPin, Calendar, DollarSign, Award, Target, BarChart3, Shield, Leaf } from "lucide-react";
import StatsCard from "../components/staff/StatsCard";
import EcosystemWheel from "../components/dashboard/EcosystemWheel";
import QuickMetrics from "../components/dashboard/QuickMetrics";
import PageHeader from "../components/common/PageHeader";
import StatsCard from "@/components/staff/StatsCard";
import EcosystemWheel from "@/components/dashboard/EcosystemWheel";
import QuickMetrics from "@/components/dashboard/QuickMetrics";
import PageHeader from "@/components/common/PageHeader";
export default function Dashboard() {
const navigate = useNavigate();

View File

@@ -9,7 +9,7 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Building2, ArrowLeft, Save, Loader2 } from "lucide-react";
import PageHeader from "../components/common/PageHeader";
import PageHeader from "@/components/common/PageHeader";
import { useToast } from "@/components/ui/use-toast";
export default function EditEnterprise() {

View File

@@ -5,7 +5,7 @@ import { useNavigate } from "react-router-dom";
import { createPageUrl } from "@/utils";
import { Button } from "@/components/ui/button";
import { ArrowLeft, Loader2 } from "lucide-react";
import EventForm from "../components/events/EventForm";
import EventForm from "@/components/events/EventForm";
export default function EditEvent() {
const navigate = useNavigate();

View File

@@ -11,7 +11,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { Textarea } from "@/components/ui/textarea";
import { Switch } from "@/components/ui/switch";
import { Briefcase, ArrowLeft, Save, Plus, X, Loader2 } from "lucide-react";
import PageHeader from "../components/common/PageHeader";
import PageHeader from "@/components/common/PageHeader";
import { useToast } from "@/components/ui/use-toast";
export default function EditPartner() {

View File

@@ -9,7 +9,7 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { MapPin, ArrowLeft, Save, Loader2 } from "lucide-react";
import PageHeader from "../components/common/PageHeader";
import PageHeader from "@/components/common/PageHeader";
import { useToast } from "@/components/ui/use-toast";
export default function EditSector() {

View File

@@ -5,7 +5,7 @@ import { useNavigate } from "react-router-dom";
import { createPageUrl } from "@/utils";
import { Button } from "@/components/ui/button";
import { ArrowLeft, Loader2 } from "lucide-react";
import StaffForm from "../components/staff/StaffForm";
import StaffForm from "@/components/staff/StaffForm";
export default function EditStaff() {
const navigate = useNavigate();

View File

@@ -10,7 +10,7 @@ import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Building2, ArrowLeft, Save, Loader2 } from "lucide-react";
import PageHeader from "../components/common/PageHeader";
import PageHeader from "@/components/common/PageHeader";
import { useToast } from "@/components/ui/use-toast";
export default function EditVendor() {

View File

@@ -8,7 +8,7 @@ import { Card, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { Building2, Plus, Search, Users, Edit } from "lucide-react";
import PageHeader from "../components/common/PageHeader";
import PageHeader from "@/components/common/PageHeader";
export default function EnterpriseManagement() {
const [searchTerm, setSearchTerm] = useState("");

View File

@@ -9,7 +9,7 @@ import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { ArrowLeft, Bell, RefreshCw } from "lucide-react";
import { format } from "date-fns";
import ShiftCard from "../components/events/ShiftCard";
import ShiftCard from "@/components/events/ShiftCard";
import {
Dialog,
DialogContent,

View File

@@ -7,18 +7,18 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Plus, Search, Calendar as CalendarIcon, Eye, Edit, Copy, X, RefreshCw } from "lucide-react";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import StatusCard from "../components/events/StatusCard";
import StatusCard from "@/components/events/StatusCard";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import { format, isSameDay, parseISO, isWithinInterval, startOfDay, endOfDay, isValid } from "date-fns";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import EventHoverCard from "../components/events/EventHoverCard";
import QuickAssignPopover from "../components/events/QuickAssignPopover";
import EventHoverCard from "@/components/events/EventHoverCard";
import QuickAssignPopover from "@/components/events/QuickAssignPopover";
import { Calendar } from "@/components/ui/calendar";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { useToast } from "@/components/ui/use-toast";
import PageHeader from "../components/common/PageHeader";
import PageHeader from "@/components/common/PageHeader";
const statusColors = {
Draft: "bg-gray-100 text-gray-800",

View File

@@ -20,7 +20,7 @@ import {
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
import PageHeader from "../components/common/PageHeader";
import PageHeader from "@/components/common/PageHeader";
export default function InviteVendor() {
const { toast } = useToast();

View File

@@ -12,7 +12,7 @@ import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { FileText, Plus, DollarSign, Search, Eye, Download } from "lucide-react";
import { format, parseISO, isPast } from "date-fns";
import PageHeader from "../components/common/PageHeader";
import PageHeader from "@/components/common/PageHeader";
import {
Dialog,
DialogContent,

View File

@@ -1,4 +1,5 @@
import React from "react";
import { Link, useLocation } from "react-router-dom";
import { createPageUrl } from "@/utils";
@@ -9,7 +10,7 @@ import {
DollarSign, Award, HelpCircle, BarChart3, Activity, Menu, MessageSquare,
Package, TrendingUp, Clipboard, LogOut, Shield, MapPin, Bell, CloudOff,
RefreshCw, User, Search, ShoppingCart, Home, Settings as SettingsIcon, MoreVertical,
Building2, Sparkles, CheckSquare, UserCheck
Building2, Sparkles, CheckSquare, UserCheck, Store
} from "lucide-react";
import { Button } from "@/components/ui/button";
import {
@@ -33,7 +34,7 @@ import RoleSwitcher from "@/components/dev/RoleSwitcher";
import NotificationPanel from "@/components/notifications/NotificationPanel";
import { Toaster } from "@/components/ui/toaster";
// Navigation items for each role (removed Control Tower)
// Navigation items for each role
const roleNavigationMap = {
admin: [
{ title: "Dashboard", url: createPageUrl("Dashboard"), icon: LayoutDashboard },
@@ -97,9 +98,9 @@ const roleNavigationMap = {
{ title: "Dashboard", url: createPageUrl("ClientDashboard"), icon: LayoutDashboard },
{ title: "My Orders", url: createPageUrl("ClientOrders"), icon: Clipboard },
{ title: "New Order", url: createPageUrl("CreateEvent"), icon: UserPlus },
{ title: "Vendors", url: createPageUrl("VendorManagement"), icon: Package },
{ title: "Vendor Marketplace", url: createPageUrl("VendorMarketplace"), icon: Store },
{ title: "Compare Rates", url: createPageUrl("VendorRateCard"), icon: DollarSign },
{ title: "Teams", url: createPageUrl("Teams"), icon: UserCheck },
{ title: "Partner Rates", url: createPageUrl("VendorRateCard"), icon: DollarSign },
{ title: "Messages", url: createPageUrl("Messages"), icon: MessageSquare },
{ title: "Invoices", url: createPageUrl("Invoices"), icon: FileText },
{ title: "Reports", url: createPageUrl("Reports"), icon: BarChart3 },
@@ -134,8 +135,6 @@ const roleNavigationMap = {
],
};
// ... keep all existing helper functions (getRoleName, etc.) ...
const getRoleName = (role) => {
const names = {
admin: "KROW Admin",
@@ -232,31 +231,32 @@ function NavigationMenu({ location, userRole, closeSheet }) {
}
export default function Layout({ children }) {
// ... keep ALL existing Layout code (state, queries, handlers) ...
const location = useLocation();
const [showNotifications, setShowNotifications] = React.useState(false);
const [mobileMenuOpen, setMobileMenuOpen] = React.useState(false);
// const { data: user } = useQuery({
// queryKey: ['current-user-layout'],
// queryFn: () => base44.auth.me(),
// });
// Mock user data to prevent redirection and allow local development
const user = {
full_name: "Dev User",
email: "dev@example.com",
user_role: "admin", // You can change this to 'procurement', 'operator', 'client', etc. to test different navigation menus
profile_picture: "https://i.pravatar.cc/150?u=a042581f4e29026024d",
};
const { data: user } = useQuery({
queryKey: ['current-user'],
queryFn: () => base44.auth.me(),
});
const sampleAvatar = "https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=400&h=400&fit=crop";
const userAvatar = user?.profile_picture || sampleAvatar;
// Get unread notification count
// const { data: unreadCount = 0 } = useQuery({
const unreadCount = 0; // Mocked value
const { data: unreadCount = 0 } = useQuery({
queryKey: ['unread-notifications', user?.id],
queryFn: async () => {
if (!user?.id) return 0;
const notifications = await base44.entities.ActivityLog.filter({
user_id: user?.id,
is_read: false
});
return notifications.length;
},
enabled: !!user?.id,
initialData: 0,
refetchInterval: 10000,
});
const userRole = user?.user_role || user?.role || "admin";
const userName = user?.full_name || user?.email || "User";
@@ -272,7 +272,6 @@ export default function Layout({ children }) {
return (
<div className="min-h-screen flex flex-col w-full bg-slate-50">
{/* ... keep ALL existing Layout structure (header, sidebar, main, footer) ... */}
<style>{`
:root {
--primary: 10 57 223;
@@ -698,3 +697,4 @@ export default function Layout({ children }) {
</div>
);
}

View File

@@ -6,9 +6,9 @@ import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { MessageSquare, Plus, Users } from "lucide-react";
import ConversationList from "../components/messaging/ConversationList";
import MessageThread from "../components/messaging/MessageThread";
import MessageInput from "../components/messaging/MessageInput";
import ConversationList from "@/components/messaging/ConversationList";
import MessageThread from "@/components/messaging/MessageThread";
import MessageInput from "@/components/messaging/MessageInput";
import {
Dialog,
DialogContent,
@@ -20,7 +20,7 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Badge } from "@/components/ui/badge";
import PageHeader from "../components/common/PageHeader";
import PageHeader from "@/components/common/PageHeader";
export default function Messages() {
const [selectedConversation, setSelectedConversation] = useState(null);

View File

@@ -8,7 +8,7 @@ import { useNavigate } from "react-router-dom";
import { createPageUrl } from "@/utils";
import { LineChart, Line, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, AreaChart, Area } from 'recharts';
import PageHeader from "../components/common/PageHeader";
import PageHeader from "@/components/common/PageHeader";
const coverageData = [
{ hub: 'San Jose', coverage: 97, incidents: 0, satisfaction: 4.9 },

View File

@@ -9,7 +9,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { Briefcase, Plus, Search, MapPin, DollarSign, Edit, Building2, TrendingUp, AlertTriangle, CheckCircle2, Users, Target, LayoutGrid, List } from "lucide-react";
import PageHeader from "../components/common/PageHeader";
import PageHeader from "@/components/common/PageHeader";
export default function PartnerManagement() {
const [searchTerm, setSearchTerm] = useState("");

View File

@@ -7,7 +7,7 @@ import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { Checkbox } from "@/components/ui/checkbox";
import { Shield, Search, Save, Info, ChevronDown, ChevronRight, Users, Calendar, Package, DollarSign, FileText, Settings as SettingsIcon, BarChart3, MessageSquare, Briefcase, Building2 } from "lucide-react";
import PageHeader from "../components/common/PageHeader";
import PageHeader from "@/components/common/PageHeader";
import { useToast } from "@/components/ui/use-toast";
import {
HoverCard,

View File

@@ -38,15 +38,15 @@ import {
Clock, // Added Clock icon for onboarding tab
X // Added X icon for closing dialog/panel
} from "lucide-react";
import StatsCard from "../components/staff/StatsCard";
import W9FormViewer from "../components/procurement/W9FormViewer";
import COIViewer from "../components/procurement/COIViewer";
import StatsCard from "@/components/staff/StatsCard";
import W9FormViewer from "@/components/procurement/W9FormViewer";
import COIViewer from "@/components/procurement/COIViewer";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import VendorDetailModal from "../components/procurement/VendorDetailModal";
import VendorHoverCard from "../components/procurement/VendorHoverCard";
import VendorScoreHoverCard from "../components/procurement/VendorScoreHoverCard"; // Added this import
import PageHeader from "../components/common/PageHeader";
import VendorDetailModal from "@/components/procurement/VendorDetailModal";
import VendorHoverCard from "@/components/procurement/VendorHoverCard";
import VendorScoreHoverCard from "@/components/procurement/VendorScoreHoverCard"; // Added this import
import PageHeader from "@/components/common/PageHeader";
import { useToast } from "@/components/ui/use-toast"; // Import useToast
export default function ProcurementDashboard() {

View File

@@ -8,7 +8,7 @@ import { Card, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { MapPin, Plus, Search, Building2, Edit } from "lucide-react";
import PageHeader from "../components/common/PageHeader";
import PageHeader from "@/components/common/PageHeader";
export default function SectorManagement() {
const [searchTerm, setSearchTerm] = useState("");

View File

@@ -27,8 +27,8 @@ import {
DialogTitle,
DialogDescription,
} from "@/components/ui/dialog";
import DragDropFileUpload from "../components/common/DragDropFileUpload";
import DocumentViewer from "../components/vendor/DocumentViewer";
import DragDropFileUpload from "@/components/common/DragDropFileUpload";
import DocumentViewer from "@/components/vendor/DocumentViewer";
// Google Places Autocomplete Component
const GoogleAddressInput = ({ value, onChange, placeholder, label, required }) => {

View File

@@ -7,10 +7,10 @@ import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { UserPlus, Users, LayoutGrid, List as ListIcon, Phone, MapPin, Calendar, Star } from "lucide-react";
import FilterBar from "../components/staff/FilterBar";
import StaffCard from "../components/staff/StaffCard";
import EmployeeCard from "../components/staff/EmployeeCard";
import PageHeader from "../components/common/PageHeader";
import FilterBar from "@/components/staff/FilterBar";
import StaffCard from "@/components/staff/StaffCard";
import EmployeeCard from "@/components/staff/EmployeeCard";
import PageHeader from "@/components/common/PageHeader";
export default function StaffDirectory() {
const [searchTerm, setSearchTerm] = useState("");

View File

@@ -9,7 +9,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { Users, Plus, Search, Building2, MapPin, UserCheck, Mail, Edit, Loader2, Trash2, UserX, LayoutGrid, List as ListIcon, RefreshCw, Send, Filter } from "lucide-react";
import PageHeader from "../components/common/PageHeader";
import PageHeader from "@/components/common/PageHeader";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";

View File

@@ -11,7 +11,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import { Users, UserPlus, Mail, Shield, Building2, Edit, Trash2 } from "lucide-react";
import { useToast } from "@/components/ui/use-toast";
import UserPermissionsModal from "../components/permissions/UserPermissionsModal"; // Import the new modal component
import UserPermissionsModal from "@/components/permissions/UserPermissionsModal"; // Import the new modal component
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; // Import Avatar components
export default function UserManagement() {

View File

@@ -21,7 +21,7 @@ import {
TrendingUp, Users, Bell, Zap, Loader2, Sparkles, FolderUp, X
} from "lucide-react";
import { differenceInDays, format, isBefore } from "date-fns";
import PageHeader from "../components/common/PageHeader";
import PageHeader from "@/components/common/PageHeader";
import { useToast } from "@/components/ui/use-toast";
export default function VendorCompliance() {

File diff suppressed because it is too large Load Diff

View File

@@ -9,8 +9,8 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
import { ArrowLeft, FileText, Shield, CheckCircle2, Clock, Eye } from "lucide-react";
import PageHeader from "../components/common/PageHeader";
import DocumentViewer from "../components/vendor/DocumentViewer";
import PageHeader from "@/components/common/PageHeader";
import DocumentViewer from "@/components/vendor/DocumentViewer";
import { useToast } from "@/components/ui/use-toast";
const ONBOARDING_DOCUMENTS = [

View File

@@ -15,9 +15,9 @@ import {
Building2, DollarSign, Mail, CheckCircle2, XCircle, Clock, Eye,
Archive, LayoutGrid, List as ListIcon
} from "lucide-react";
import PageHeader from "../components/common/PageHeader";
import VendorScoreHoverCard from "../components/procurement/VendorScoreHoverCard";
import VendorDetailModal from "../components/procurement/VendorDetailModal";
import PageHeader from "@/components/common/PageHeader";
import VendorScoreHoverCard from "@/components/procurement/VendorScoreHoverCard";
import VendorDetailModal from "@/components/procurement/VendorDetailModal";
import { useToast } from "@/components/ui/use-toast";
import {
Dialog,

View File

@@ -0,0 +1,922 @@
import React, { useState, useMemo } from "react";
import { base44 } from "@/api/base44Client";
import { useQuery } from "@tanstack/react-query";
import { Link, useNavigate } from "react-router-dom";
import { createPageUrl } from "@/utils";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from "@/components/ui/dialog";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { Textarea } from "@/components/ui/textarea";
import { Search, MapPin, Users, Star, DollarSign, TrendingUp, MessageSquare, CheckCircle, Award, Filter, Grid, List, Phone, Mail, Building2, Zap, ArrowRight, ChevronDown, ChevronUp, UserCheck, Briefcase } from "lucide-react";
import { useToast } from "@/components/ui/use-toast";
export default function VendorMarketplace() {
const navigate = useNavigate();
const { toast } = useToast();
const [searchTerm, setSearchTerm] = useState("");
const [regionFilter, setRegionFilter] = useState("all");
const [categoryFilter, setCategoryFilter] = useState("all");
const [sortBy, setSortBy] = useState("rating");
const [viewMode, setViewMode] = useState("grid");
const [contactModal, setContactModal] = useState({ open: false, vendor: null });
const [message, setMessage] = useState("");
const [expandedVendors, setExpandedVendors] = useState({});
const { data: user } = useQuery({
queryKey: ['current-user-marketplace'],
queryFn: () => base44.auth.me(),
});
const { data: vendors = [] } = useQuery({
queryKey: ['approved-vendors'],
queryFn: async () => {
const allVendors = await base44.entities.Vendor.list();
return allVendors.filter(v => v.approval_status === 'approved' && v.is_active);
},
});
const { data: vendorRates = [] } = useQuery({
queryKey: ['vendor-rates-marketplace'],
queryFn: () => base44.entities.VendorRate.list(),
});
const { data: staff = [] } = useQuery({
queryKey: ['vendor-staff-count'],
queryFn: () => base44.entities.Staff.list(),
});
const { data: events = [] } = useQuery({
queryKey: ['events-vendor-marketplace'],
queryFn: () => base44.entities.Event.list(),
initialData: [],
});
const { data: businesses = [] } = useQuery({
queryKey: ['businesses-vendor-marketplace'],
queryFn: () => base44.entities.Business.list(),
initialData: [],
});
// Calculate vendor metrics
const vendorsWithMetrics = useMemo(() => {
return vendors.map(vendor => {
const rates = vendorRates.filter(r => r.vendor_name === vendor.legal_name || r.vendor_id === vendor.id);
const vendorStaff = staff.filter(s => s.vendor_name === vendor.legal_name);
const avgRate = rates.length > 0
? rates.reduce((sum, r) => sum + (r.client_rate || 0), 0) / rates.length
: 0;
const minRate = rates.length > 0
? Math.min(...rates.map(r => r.client_rate || 999))
: 0;
const rating = 4.5 + (Math.random() * 0.5);
const completedJobs = Math.floor(Math.random() * 200) + 50;
// Calculate how many clients in user's sector are using this vendor
const vendorEvents = events.filter(e =>
e.vendor_name === vendor.legal_name ||
e.vendor_id === vendor.id
);
const uniqueClients = new Set(
vendorEvents.map(e => e.business_name || e.client_email)
).size;
// Calculate sector-specific usage
const userSector = user?.sector || user?.company_name;
const sectorClients = businesses.filter(b =>
b.sector === userSector || b.area === user?.area
);
const clientsInSector = new Set(
vendorEvents
.filter(e => sectorClients.some(sc =>
sc.business_name === e.business_name ||
sc.contact_name === e.client_name
))
.map(e => e.business_name || e.client_email)
).size;
// Group rates by category
const ratesByCategory = rates.reduce((acc, rate) => {
const category = rate.category || 'Other';
if (!acc[category]) {
acc[category] = [];
}
acc[category].push(rate);
return acc;
}, {});
return {
...vendor,
rates,
ratesByCategory,
avgRate,
minRate,
rating,
completedJobs,
staffCount: vendorStaff.length,
responseTime: `${Math.floor(Math.random() * 3) + 1}h`,
totalClients: uniqueClients,
clientsInSector: clientsInSector,
};
});
}, [vendors, vendorRates, staff, events, businesses, user]);
// Filtering and sorting
const filteredVendors = useMemo(() => {
let filtered = vendorsWithMetrics;
// Search
if (searchTerm) {
const lower = searchTerm.toLowerCase();
filtered = filtered.filter(v =>
v.legal_name?.toLowerCase().includes(lower) ||
v.doing_business_as?.toLowerCase().includes(lower) ||
v.service_specialty?.toLowerCase().includes(lower)
);
}
// Region filter
if (regionFilter !== "all") {
filtered = filtered.filter(v => v.region === regionFilter);
}
// Category filter
if (categoryFilter !== "all") {
filtered = filtered.filter(v => v.service_specialty === categoryFilter);
}
// Sorting
filtered.sort((a, b) => {
switch (sortBy) {
case "rating":
return b.rating - a.rating;
case "price-low":
return a.minRate - b.minRate;
case "price-high":
return b.avgRate - a.avgRate;
case "staff":
return b.staffCount - a.staffCount;
default:
return 0;
}
});
return filtered;
}, [vendorsWithMetrics, searchTerm, regionFilter, categoryFilter, sortBy]);
const uniqueRegions = [...new Set(vendors.map(v => v.region).filter(Boolean))];
const uniqueCategories = [...new Set(vendors.map(v => v.service_specialty).filter(Boolean))];
const handleContactVendor = (vendor) => {
setContactModal({ open: true, vendor });
setMessage(`Hi ${vendor.legal_name},\n\nI'm interested in your services for an upcoming event. Could you please provide more information about your availability and pricing?\n\nBest regards,\n${user?.full_name || 'Client'}`);
};
const handleSendMessage = async () => {
if (!message.trim()) {
toast({
title: "Message required",
description: "Please enter a message to send.",
variant: "destructive",
});
return;
}
// Create conversation
try {
await base44.entities.Conversation.create({
participants: [
{ id: user?.id, name: user?.full_name, role: "client" },
{ id: contactModal.vendor.id, name: contactModal.vendor.legal_name, role: "vendor" }
],
conversation_type: "client-vendor",
is_group: false,
subject: `Inquiry from ${user?.full_name || 'Client'}`,
last_message: message.substring(0, 100),
last_message_at: new Date().toISOString(),
status: "active"
});
toast({
title: "✅ Message sent!",
description: `Your message has been sent to ${contactModal.vendor.legal_name}`,
});
setContactModal({ open: false, vendor: null });
setMessage("");
} catch (error) {
toast({
title: "Failed to send message",
description: error.message,
variant: "destructive",
});
}
};
const handleCreateOrder = (vendor) => {
sessionStorage.setItem('selectedVendor', JSON.stringify({
id: vendor.id,
name: vendor.legal_name,
rates: vendor.rates
}));
navigate(createPageUrl("CreateEvent"));
toast({
title: "Vendor selected",
description: `${vendor.legal_name} will be used for this order.`,
});
};
const toggleVendorRates = (vendorId) => {
setExpandedVendors(prev => ({
...prev,
[vendorId]: !prev[vendorId]
}));
};
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/20 to-indigo-50/30 p-6">
<div className="max-w-[1800px] mx-auto space-y-8">
{/* Hero Header */}
<div className="relative overflow-hidden bg-gradient-to-r from-[#0A39DF] via-blue-600 to-indigo-600 rounded-2xl p-6 shadow-xl">
<div className="absolute inset-0 opacity-10" style={{
backgroundImage: 'radial-gradient(circle at 2px 2px, white 1px, transparent 0)',
backgroundSize: '40px 40px'
}} />
<div className="relative z-10 flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-white/20 backdrop-blur-sm rounded-xl flex items-center justify-center">
<Building2 className="w-6 h-6 text-white" />
</div>
<div>
<h1 className="text-3xl font-bold text-white mb-1">Vendor Marketplace</h1>
<p className="text-blue-100 text-sm">Compare rates See who others trust Order instantly</p>
</div>
</div>
<Badge className="bg-white/20 backdrop-blur-md text-white border-white/30 border px-4 py-2 text-base font-semibold">
{filteredVendors.length} Vendors
</Badge>
</div>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-4 gap-4">
<Card className="bg-gradient-to-br from-[#0A39DF] to-blue-600 text-white border-0 shadow-md hover:shadow-lg transition-all hover:scale-105 overflow-hidden relative group">
<div className="absolute top-0 right-0 w-24 h-24 bg-white/10 rounded-full -mr-12 -mt-12 group-hover:scale-110 transition-transform" />
<CardContent className="p-5 relative z-10">
<div className="flex items-center justify-between">
<div>
<p className="text-blue-100 text-xs mb-2 font-medium uppercase tracking-wide">Vendors</p>
<p className="text-3xl font-bold mb-0.5">{vendors.length}</p>
<p className="text-blue-200 text-xs">Approved</p>
</div>
<div className="w-12 h-12 bg-white/20 backdrop-blur-sm rounded-xl flex items-center justify-center">
<Building2 className="w-6 h-6" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-emerald-500 to-green-600 text-white border-0 shadow-md hover:shadow-lg transition-all hover:scale-105 overflow-hidden relative group">
<div className="absolute top-0 right-0 w-24 h-24 bg-white/10 rounded-full -mr-12 -mt-12 group-hover:scale-110 transition-transform" />
<CardContent className="p-5 relative z-10">
<div className="flex items-center justify-between">
<div>
<p className="text-emerald-100 text-xs mb-2 font-medium uppercase tracking-wide">Staff</p>
<p className="text-3xl font-bold mb-0.5">{staff.length}</p>
<p className="text-emerald-200 text-xs">Available</p>
</div>
<div className="w-12 h-12 bg-white/20 backdrop-blur-sm rounded-xl flex items-center justify-center">
<Users className="w-6 h-6" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-indigo-500 to-purple-600 text-white border-0 shadow-md hover:shadow-lg transition-all hover:scale-105 overflow-hidden relative group">
<div className="absolute top-0 right-0 w-24 h-24 bg-white/10 rounded-full -mr-12 -mt-12 group-hover:scale-110 transition-transform" />
<CardContent className="p-5 relative z-10">
<div className="flex items-center justify-between">
<div>
<p className="text-indigo-100 text-xs mb-2 font-medium uppercase tracking-wide">Avg Rate</p>
<p className="text-3xl font-bold mb-0.5">
${Math.round(vendorsWithMetrics.reduce((sum, v) => sum + v.avgRate, 0) / vendorsWithMetrics.length || 0)}
</p>
<p className="text-indigo-200 text-xs">Per hour</p>
</div>
<div className="w-12 h-12 bg-white/20 backdrop-blur-sm rounded-xl flex items-center justify-center">
<DollarSign className="w-6 h-6" />
</div>
</div>
</CardContent>
</Card>
<Card className="bg-gradient-to-br from-amber-500 to-orange-500 text-white border-0 shadow-md hover:shadow-lg transition-all hover:scale-105 overflow-hidden relative group">
<div className="absolute top-0 right-0 w-24 h-24 bg-white/10 rounded-full -mr-12 -mt-12 group-hover:scale-110 transition-transform" />
<CardContent className="p-5 relative z-10">
<div className="flex items-center justify-between">
<div>
<p className="text-amber-100 text-xs mb-2 font-medium uppercase tracking-wide">Rating</p>
<div className="flex items-center gap-2 mb-0.5">
<p className="text-3xl font-bold">4.7</p>
<Star className="w-5 h-5 fill-white" />
</div>
<p className="text-amber-200 text-xs">Average</p>
</div>
<div className="w-12 h-12 bg-white/20 backdrop-blur-sm rounded-xl flex items-center justify-center">
<Award className="w-6 h-6" />
</div>
</div>
</CardContent>
</Card>
</div>
{/* Enhanced Filters */}
<Card className="border-2 border-slate-200 shadow-lg bg-white/80 backdrop-blur-sm">
<CardContent className="p-6">
<div className="grid grid-cols-12 gap-4 items-end">
{/* Search - Takes more space */}
<div className="col-span-5">
<label className="text-xs font-bold text-slate-700 mb-2 block uppercase tracking-wide">
Search Vendors
</label>
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[#0A39DF]" />
<Input
placeholder="Search by name, specialty, or location..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10 h-11 border-2 border-slate-200 focus:border-[#0A39DF] text-sm rounded-lg shadow-sm"
/>
</div>
</div>
{/* Region Filter */}
<div className="col-span-2">
<label className="text-xs font-bold text-slate-700 mb-2 block uppercase tracking-wide">
Region
</label>
<Select value={regionFilter} onValueChange={setRegionFilter}>
<SelectTrigger className="h-11 border-2 border-slate-200 rounded-lg shadow-sm">
<SelectValue placeholder="All Regions" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Regions</SelectItem>
{uniqueRegions.map(region => (
<SelectItem key={region} value={region}>{region}</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Category Filter */}
<div className="col-span-2">
<label className="text-xs font-bold text-slate-700 mb-2 block uppercase tracking-wide">
Specialty
</label>
<Select value={categoryFilter} onValueChange={setCategoryFilter}>
<SelectTrigger className="h-11 border-2 border-slate-200 rounded-lg shadow-sm">
<SelectValue placeholder="All" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Specialties</SelectItem>
{uniqueCategories.map(cat => (
<SelectItem key={cat} value={cat}>{cat}</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Sort */}
<div className="col-span-2">
<label className="text-xs font-bold text-slate-700 mb-2 block uppercase tracking-wide">
Sort By
</label>
<Select value={sortBy} onValueChange={setSortBy}>
<SelectTrigger className="h-11 border-2 border-slate-200 rounded-lg shadow-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="rating"> Highest Rated</SelectItem>
<SelectItem value="price-low">💰 Lowest Price</SelectItem>
<SelectItem value="price-high">💎 Premium</SelectItem>
<SelectItem value="staff">👥 Most Staff</SelectItem>
</SelectContent>
</Select>
</div>
{/* View Toggle */}
<div className="col-span-1 flex justify-end">
<div className="flex gap-1 bg-slate-100 p-1 rounded-lg">
<Button
variant={viewMode === "grid" ? "default" : "ghost"}
size="sm"
onClick={() => setViewMode("grid")}
className={viewMode === "grid" ? "bg-[#0A39DF] hover:bg-[#0A39DF]" : "hover:bg-white"}
>
<Grid className="w-4 h-4" />
</Button>
<Button
variant={viewMode === "list" ? "default" : "ghost"}
size="sm"
onClick={() => setViewMode("list")}
className={viewMode === "list" ? "bg-[#0A39DF] hover:bg-[#0A39DF]" : "hover:bg-white"}
>
<List className="w-4 h-4" />
</Button>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Vendors Grid/List */}
{viewMode === "grid" ? (
<div className="space-y-8">
{filteredVendors.map((vendor) => {
const isExpanded = expandedVendors[vendor.id];
return (
<Card key={vendor.id} className="bg-white border-2 border-slate-200 hover:border-[#0A39DF] hover:shadow-2xl transition-all group overflow-hidden">
{/* Vendor Header */}
<CardHeader className="bg-gradient-to-r from-slate-50 to-blue-50/30 border-b border-slate-200 pb-5">
<div className="flex items-start justify-between gap-6">
<div className="flex items-center gap-4 flex-1">
<div className="relative">
<Avatar className="w-16 h-16 bg-gradient-to-br from-[#0A39DF] to-indigo-600 shadow-lg ring-2 ring-blue-200">
<AvatarFallback className="text-white text-xl font-bold">
{vendor.legal_name?.charAt(0)}
</AvatarFallback>
</Avatar>
<div className="absolute -bottom-1 -right-1 w-6 h-6 bg-green-500 rounded-full border-2 border-white flex items-center justify-center shadow-md">
<CheckCircle className="w-3.5 h-3.5 text-white" />
</div>
</div>
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<CardTitle className="text-xl font-bold text-[#1C323E] group-hover:text-[#0A39DF] transition-colors">
{vendor.legal_name}
</CardTitle>
<div className="flex items-center gap-1.5 bg-amber-50 px-3 py-1.5 rounded-full border border-amber-200">
<Star className="w-4 h-4 text-amber-600 fill-amber-600" />
<span className="text-sm font-bold text-amber-700">{vendor.rating.toFixed(1)}</span>
</div>
</div>
{vendor.doing_business_as && (
<p className="text-xs text-slate-500 mb-3 italic">DBA: {vendor.doing_business_as}</p>
)}
<div className="flex items-center gap-4 flex-wrap">
{vendor.service_specialty && (
<div className="flex items-center gap-1.5 text-sm bg-blue-50 px-3 py-1.5 rounded-lg border border-blue-200">
<Award className="w-3.5 h-3.5 text-[#0A39DF]" />
<span className="text-[#1C323E] font-semibold">{vendor.service_specialty}</span>
</div>
)}
<div className="flex items-center gap-1.5 text-sm">
<MapPin className="w-4 h-4 text-[#0A39DF]" />
<span className="text-slate-700 font-medium">{vendor.region || vendor.city}</span>
</div>
<div className="flex items-center gap-1.5 text-sm">
<Users className="w-4 h-4 text-[#0A39DF]" />
<span className="text-slate-700 font-medium">{vendor.staffCount} Staff</span>
</div>
<div className="flex items-center gap-1.5 text-sm">
<Zap className="w-4 h-4 text-emerald-600" />
<span className="text-slate-700 font-medium">{vendor.responseTime}</span>
</div>
</div>
</div>
</div>
<div className="flex flex-col items-end gap-3">
<div className="p-4 bg-gradient-to-br from-[#0A39DF] to-indigo-600 rounded-xl shadow-lg text-center min-w-[140px]">
<p className="text-blue-100 text-[10px] mb-1 font-semibold uppercase tracking-wide">Starting from</p>
<p className="text-3xl font-bold text-white mb-1">${vendor.minRate}</p>
<p className="text-blue-200 text-xs">per hour</p>
</div>
{/* Social Proof */}
{vendor.clientsInSector > 0 && (
<div className="bg-gradient-to-br from-purple-50 to-pink-50 border-2 border-purple-300 rounded-xl px-4 py-3 shadow-md min-w-[140px]">
<div className="flex items-center justify-center gap-2 mb-1">
<UserCheck className="w-5 h-5 text-purple-700" />
<span className="text-2xl font-bold text-purple-700">{vendor.clientsInSector}</span>
</div>
<p className="text-[10px] text-purple-600 font-bold text-center uppercase tracking-wide">
clients in your area
</p>
</div>
)}
<div className="flex items-center gap-2">
<Badge className="bg-green-50 text-green-700 border-green-200 border px-3 py-1.5 text-xs">
<CheckCircle className="w-3 h-3 mr-1" />
{vendor.completedJobs} jobs
</Badge>
<Badge variant="outline" className="border-slate-300 px-3 py-1.5 text-xs font-semibold">
{vendor.rates.length} services
</Badge>
</div>
</div>
</div>
</CardHeader>
{/* Actions Bar */}
<div className="px-5 py-4 bg-white border-b border-slate-100">
<div className="flex items-center justify-between">
<Collapsible open={isExpanded} onOpenChange={() => toggleVendorRates(vendor.id)} className="flex-1">
<CollapsibleTrigger asChild>
<Button
variant="ghost"
className="w-auto px-4 py-2 hover:bg-blue-50 rounded-lg group/trigger"
>
<div className="flex items-center gap-3">
<div className="w-9 h-9 bg-blue-100 rounded-lg flex items-center justify-center group-hover/trigger:bg-[#0A39DF] transition-colors">
<TrendingUp className="w-4 h-4 text-[#0A39DF] group-hover/trigger:text-white transition-colors" />
</div>
<div className="text-left">
<span className="font-bold text-[#1C323E] text-base block">
Compare All Rates
</span>
<span className="text-xs text-slate-500">
{vendor.rates.length} services Full breakdown
</span>
</div>
{isExpanded ? (
<ChevronUp className="w-4 h-4 text-slate-400 ml-2" />
) : (
<ChevronDown className="w-4 h-4 text-slate-400 ml-2" />
)}
</div>
</Button>
</CollapsibleTrigger>
</Collapsible>
<div className="flex items-center gap-2">
<Button
variant="outline"
onClick={() => handleContactVendor(vendor)}
className="border-2 hover:border-[#0A39DF] hover:bg-blue-50 rounded-lg"
>
<MessageSquare className="w-4 h-4 mr-2" />
Contact
</Button>
<Button
onClick={() => handleCreateOrder(vendor)}
className="bg-gradient-to-r from-emerald-600 to-green-600 hover:from-emerald-700 hover:to-green-700 shadow-md rounded-lg"
>
<Zap className="w-4 h-4 mr-2" />
Create Order
</Button>
</div>
</div>
</div>
{/* Rate Comparison Section */}
<Collapsible open={isExpanded}>
<CollapsibleContent>
<CardContent className="p-6 bg-gradient-to-br from-slate-50 to-blue-50/20">
<div className="space-y-4">
{Object.entries(vendor.ratesByCategory).map(([category, categoryRates], catIdx) => (
<div key={category} className="bg-white border border-slate-200 rounded-xl overflow-hidden shadow-sm hover:shadow-md transition-all">
<div className="bg-gradient-to-r from-[#0A39DF] to-indigo-600 px-5 py-3">
<h4 className="font-bold text-white text-sm flex items-center gap-2">
<div className="w-7 h-7 bg-white/20 rounded-lg flex items-center justify-center">
<Briefcase className="w-4 h-4 text-white" />
</div>
{category}
<Badge className="bg-white/20 text-white border-white/30 border ml-auto px-2 py-1 text-xs">
{categoryRates.length}
</Badge>
</h4>
</div>
<div className="divide-y divide-slate-100">
{categoryRates.map((rate, rateIdx) => {
const baseWage = rate.employee_wage || 0;
const markupAmount = baseWage * ((rate.markup_percentage || 0) / 100);
const subtotal = baseWage + markupAmount;
const feeAmount = subtotal * ((rate.vendor_fee_percentage || 0) / 100);
return (
<div key={rate.id} className="p-4 hover:bg-blue-50 transition-all">
<div className="flex items-center justify-between gap-6">
<div className="flex-1">
<div className="flex items-center gap-2 mb-3">
<div className="w-7 h-7 bg-slate-100 rounded-lg flex items-center justify-center font-bold text-slate-600 text-sm">
{rateIdx + 1}
</div>
<h5 className="font-bold text-[#1C323E] text-base">
{rate.role_name}
</h5>
</div>
{/* Visual Price Breakdown */}
<div className="space-y-2">
<div className="flex items-center gap-2 text-xs">
<span className="w-24 text-slate-600 font-medium">Base Wage:</span>
<div className="flex-1 h-7 bg-gradient-to-r from-emerald-500 to-emerald-600 rounded-lg flex items-center justify-between px-3 text-white font-semibold shadow-sm">
<span>${baseWage.toFixed(2)}/hr</span>
<span className="text-emerald-100 text-[10px]">Employee</span>
</div>
</div>
<div className="flex items-center gap-2 text-xs">
<span className="w-24 text-slate-600 font-medium">+ Markup:</span>
<div className="flex-1 h-7 bg-gradient-to-r from-blue-500 to-blue-600 rounded-lg flex items-center justify-between px-3 text-white font-semibold shadow-sm">
<span>{rate.markup_percentage}%</span>
<span className="text-blue-100 text-[10px]">+${markupAmount.toFixed(2)}</span>
</div>
</div>
<div className="flex items-center gap-2 text-xs">
<span className="w-24 text-slate-600 font-medium">+ Admin Fee:</span>
<div className="flex-1 h-7 bg-gradient-to-r from-purple-500 to-purple-600 rounded-lg flex items-center justify-between px-3 text-white font-semibold shadow-sm">
<span>{rate.vendor_fee_percentage}%</span>
<span className="text-purple-100 text-[10px]">+${feeAmount.toFixed(2)}</span>
</div>
</div>
</div>
</div>
<div className="flex flex-col items-center justify-center">
<p className="text-[10px] text-slate-500 mb-2 font-bold uppercase tracking-wide">You Pay</p>
<div className="bg-gradient-to-br from-[#0A39DF] to-indigo-600 rounded-xl px-6 py-4 shadow-lg">
<p className="text-3xl font-bold text-white">
${rate.client_rate?.toFixed(0)}
</p>
<p className="text-blue-200 text-xs text-center mt-1">per hour</p>
</div>
</div>
</div>
</div>
);
})}
</div>
</div>
))}
{/* Summary Footer */}
<div className="mt-5 p-5 bg-white rounded-xl border-2 border-blue-200 shadow-sm">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-[#0A39DF] rounded-lg flex items-center justify-center">
<DollarSign className="w-5 h-5 text-white" />
</div>
<div>
<p className="text-xs text-slate-600 font-medium">Average Rate</p>
<p className="text-2xl font-bold text-[#1C323E]">
${Math.round(vendor.avgRate)}/hr
</p>
</div>
</div>
<div className="text-right">
<p className="text-xs text-slate-600 font-medium">Price Range</p>
<p className="text-xl font-bold text-[#1C323E]">
${vendor.minRate} - ${Math.round(Math.max(...vendor.rates.map(r => r.client_rate || 0)))}
</p>
</div>
</div>
</div>
</div>
</CardContent>
</CollapsibleContent>
</Collapsible>
</Card>
);
})}
</div>
) : (
<Card className="border-2 border-slate-200 shadow-xl">
<CardContent className="p-0">
<table className="w-full">
<thead className="bg-gradient-to-r from-slate-50 to-blue-50 border-b-2 border-slate-200">
<tr>
<th className="text-left py-5 px-5 text-xs font-bold text-slate-700 uppercase tracking-wider">Vendor</th>
<th className="text-left py-5 px-5 text-xs font-bold text-slate-700 uppercase tracking-wider">Specialty</th>
<th className="text-left py-5 px-5 text-xs font-bold text-slate-700 uppercase tracking-wider">Location</th>
<th className="text-center py-5 px-5 text-xs font-bold text-slate-700 uppercase tracking-wider">Rating</th>
<th className="text-center py-5 px-5 text-xs font-bold text-slate-700 uppercase tracking-wider">Clients</th>
<th className="text-center py-5 px-5 text-xs font-bold text-slate-700 uppercase tracking-wider">Staff</th>
<th className="text-center py-5 px-5 text-xs font-bold text-slate-700 uppercase tracking-wider">Min Rate</th>
<th className="text-center py-5 px-5 text-xs font-bold text-slate-700 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody>
{filteredVendors.map((vendor) => (
<tr key={vendor.id} className="border-b border-slate-100 hover:bg-gradient-to-r hover:from-blue-50 hover:to-transparent transition-all group">
<td className="py-5 px-5">
<div className="flex items-center gap-3">
<Avatar className="w-12 h-12 bg-gradient-to-br from-[#0A39DF] to-indigo-600 ring-2 ring-slate-200 group-hover:ring-[#0A39DF] transition-all shadow-md">
<AvatarFallback className="text-white font-bold text-lg">
{vendor.legal_name?.charAt(0)}
</AvatarFallback>
</Avatar>
<div>
<p className="font-bold text-[#1C323E] text-base">{vendor.legal_name}</p>
<p className="text-xs text-slate-500 flex items-center gap-1 mt-1">
<CheckCircle className="w-3 h-3 text-green-600" />
{vendor.completedJobs} jobs completed
</p>
</div>
</div>
</td>
<td className="py-5 px-5">
<span className="text-sm text-slate-700 font-medium">{vendor.service_specialty || '—'}</span>
</td>
<td className="py-5 px-5">
<div className="flex items-center gap-2 text-sm text-slate-700">
<MapPin className="w-4 h-4 text-[#0A39DF]" />
<span className="font-medium">{vendor.region}</span>
</div>
</td>
<td className="py-5 px-5 text-center">
<div className="inline-flex items-center gap-2 bg-amber-50 px-4 py-2 rounded-full shadow-sm">
<Star className="w-4 h-4 text-amber-500 fill-amber-500" />
<span className="font-bold text-base text-amber-700">{vendor.rating.toFixed(1)}</span>
</div>
</td>
<td className="py-5 px-5 text-center">
{vendor.clientsInSector > 0 ? (
<Badge className="bg-purple-100 text-purple-700 border-purple-200 border-2 font-bold text-sm px-3 py-1.5">
<UserCheck className="w-3.5 h-3.5 mr-1.5" />
{vendor.clientsInSector}
</Badge>
) : (
<span className="text-slate-400 text-sm"></span>
)}
</td>
<td className="py-5 px-5 text-center">
<Badge variant="outline" className="font-bold border-2 text-sm px-3 py-1.5">{vendor.staffCount}</Badge>
</td>
<td className="py-5 px-5 text-center">
<div className="inline-flex flex-col items-center bg-blue-50 px-5 py-3 rounded-xl shadow-sm">
<span className="font-bold text-2xl text-[#0A39DF]">${vendor.minRate}</span>
<span className="text-xs text-slate-500 font-medium">/hour</span>
</div>
</td>
<td className="py-5 px-5">
<div className="flex items-center justify-center gap-2">
<Button
size="sm"
variant="outline"
onClick={() => handleContactVendor(vendor)}
className="border-2 hover:border-[#0A39DF] hover:bg-blue-50 rounded-lg"
>
<MessageSquare className="w-4 h-4 mr-1" />
Contact
</Button>
<Button
size="sm"
onClick={() => {
toggleVendorRates(vendor.id);
setViewMode("grid");
}}
className="bg-[#0A39DF] hover:bg-blue-700 rounded-lg"
>
<TrendingUp className="w-4 h-4 mr-1" />
Rates
</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</CardContent>
</Card>
)}
{filteredVendors.length === 0 && (
<div className="text-center py-16 bg-white rounded-2xl border-2 border-dashed border-slate-300">
<div className="w-20 h-20 mx-auto mb-4 bg-slate-100 rounded-2xl flex items-center justify-center">
<Building2 className="w-10 h-10 text-slate-400" />
</div>
<h3 className="text-xl font-bold text-slate-900 mb-2">No vendors found</h3>
<p className="text-slate-600 mb-5">Try adjusting your filters to see more results</p>
<Button
variant="outline"
onClick={() => {
setSearchTerm("");
setRegionFilter("all");
setCategoryFilter("all");
}}
className="border-2 hover:border-[#0A39DF] hover:bg-blue-50 rounded-lg"
>
Clear All Filters
</Button>
</div>
)}
</div>
{/* Contact Modal */}
<Dialog open={contactModal.open} onOpenChange={(open) => setContactModal({ open, vendor: null })}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle className="text-2xl font-bold text-[#1C323E] mb-1">
Contact {contactModal.vendor?.legal_name}
</DialogTitle>
<DialogDescription>
Start a conversation and get staffing help within hours
</DialogDescription>
</DialogHeader>
<div className="space-y-5 py-4">
{/* Vendor Info Card */}
<div className="flex items-center gap-4 p-5 bg-gradient-to-r from-blue-50 to-indigo-50 rounded-xl border border-blue-200">
<Avatar className="w-16 h-16 bg-gradient-to-br from-[#0A39DF] to-indigo-600 ring-2 ring-white shadow-md">
<AvatarFallback className="text-white text-xl font-bold">
{contactModal.vendor?.legal_name?.charAt(0)}
</AvatarFallback>
</Avatar>
<div className="flex-1">
<h4 className="font-bold text-[#1C323E] text-lg mb-2">{contactModal.vendor?.legal_name}</h4>
<div className="flex items-center gap-2 flex-wrap">
<div className="flex items-center gap-1.5 text-xs bg-white px-2 py-1.5 rounded-lg">
<MapPin className="w-3.5 h-3.5 text-[#0A39DF]" />
<span className="font-medium text-slate-700">{contactModal.vendor?.region}</span>
</div>
<div className="flex items-center gap-1.5 text-xs bg-white px-2 py-1.5 rounded-lg">
<Users className="w-3.5 h-3.5 text-[#0A39DF]" />
<span className="font-medium text-slate-700">{contactModal.vendor?.staffCount} staff</span>
</div>
<div className="flex items-center gap-1.5 bg-amber-50 px-2 py-1.5 rounded-lg">
<Star className="w-3.5 h-3.5 text-amber-500 fill-amber-500" />
<span className="text-xs font-bold text-amber-700">{contactModal.vendor?.rating?.toFixed(1)}</span>
</div>
{contactModal.vendor?.clientsInSector > 0 && (
<Badge className="bg-purple-100 text-purple-700 border-purple-200 border text-xs">
<UserCheck className="w-3 h-3 mr-1" />
{contactModal.vendor?.clientsInSector} in your area
</Badge>
)}
</div>
</div>
</div>
{/* Message Input */}
<div>
<label className="text-sm font-bold text-slate-700 mb-2 block flex items-center gap-2">
<MessageSquare className="w-4 h-4 text-[#0A39DF]" />
Your Message
</label>
<Textarea
value={message}
onChange={(e) => setMessage(e.target.value)}
rows={8}
placeholder="Enter your message..."
className="resize-none border-2 border-slate-200 focus:border-[#0A39DF] text-sm rounded-lg"
/>
<div className="text-xs text-slate-500 mt-2 bg-blue-50 p-2.5 rounded-lg border border-blue-100">
💡 <strong>Tip:</strong> Include event date, location, and number of staff needed for faster response
</div>
</div>
</div>
<DialogFooter className="gap-2">
<Button
variant="outline"
onClick={() => setContactModal({ open: false, vendor: null })}
className="border-2 rounded-lg"
>
Cancel
</Button>
<Button
onClick={handleSendMessage}
className="bg-gradient-to-r from-[#0A39DF] to-indigo-600 hover:from-blue-700 hover:to-indigo-700 shadow-md rounded-lg"
>
<MessageSquare className="w-4 h-4 mr-2" />
Send Message
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}

View File

@@ -23,7 +23,7 @@ import {
FileCheck,
TrendingUp
} from "lucide-react";
import PageHeader from "../components/common/PageHeader";
import PageHeader from "@/components/common/PageHeader";
import { useToast } from "@/components/ui/use-toast";
const ONBOARDING_STEPS = [

View File

@@ -26,7 +26,7 @@ import { format, isToday, addDays } from "date-fns";
import { useToast } from "@/components/ui/use-toast";
import { Link, useNavigate } from "react-router-dom";
import { createPageUrl } from "@/utils";
import EventAssignmentModal from "../components/events/EventAssignmentModal";
import EventAssignmentModal from "@/components/events/EventAssignmentModal";
const getStatusColor = (order) => {
// Check for Rapid Request first

View File

@@ -6,7 +6,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Download, Search, Building2, MapPin, DollarSign } from "lucide-react";
import PageHeader from "../components/common/PageHeader";
import PageHeader from "@/components/common/PageHeader";
import { Input } from "@/components/ui/input";
// Define regions - these map to the notes field or can be a new field

View File

@@ -8,7 +8,7 @@ import { Button } from "@/components/ui/button";
import { Users, MapPin, DollarSign, Award, BookOpen, TrendingUp, Star, Clock, ArrowLeft, Calendar } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { createPageUrl } from "@/utils";
import PageHeader from "../components/common/PageHeader";
import PageHeader from "@/components/common/PageHeader";
export default function WorkforceDashboard() {
const navigate = useNavigate();

View File

@@ -118,6 +118,8 @@ import VendorRates from "./VendorRates";
import VendorDocumentReview from "./VendorDocumentReview";
import VendorMarketplace from "./VendorMarketplace";
import { BrowserRouter as Router, Route, Routes, useLocation } from 'react-router-dom';
const PAGES = {
@@ -240,6 +242,8 @@ const PAGES = {
VendorDocumentReview: VendorDocumentReview,
VendorMarketplace: VendorMarketplace,
}
function _getCurrentPage(url) {
@@ -385,6 +389,8 @@ function PagesContent() {
<Route path="/VendorDocumentReview" element={<VendorDocumentReview />} />
<Route path="/VendorMarketplace" element={<VendorMarketplace />} />
</Routes>
</Layout>
);