Files
Krow-workspace/src/pages/VendorManagement.jsx

1148 lines
70 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState } from "react";
import { base44 } from "@/api/base44Client";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Link } from "react-router-dom";
import { createPageUrl } from "@/utils";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import {
Package, Plus, Search, Users, Edit, ChevronLeft, ChevronRight,
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 { useToast } from "@/components/ui/use-toast";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import { Textarea } from "@/components/ui/textarea";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
export default function VendorManagement() {
const [searchTerm, setSearchTerm] = useState("");
const [regionFilter, setRegionFilter] = useState("all");
const [stateFilter, setStateFilter] = useState("all");
const [specialtyFilter, setSpecialtyFilter] = useState("all");
const [softwareFilter, setSoftwareFilter] = useState("all");
const [statusFilter, setStatusFilter] = useState("all");
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(10);
const [editingVendor, setEditingVendor] = useState(null);
const [showEditModal, setShowEditModal] = useState(false);
const [reviewingVendor, setReviewingVendor] = useState(null);
const [showReviewModal, setShowReviewModal] = useState(false);
const [showRejectModal, setShowRejectModal] = useState(false);
const [rejectionReason, setRejectionReason] = useState("");
const [viewMode, setViewMode] = useState("list"); // "list" or "grid"
const { toast } = useToast();
const queryClient = useQueryClient();
const { data: user } = useQuery({
queryKey: ['current-user-vendor-mgmt'],
queryFn: () => base44.auth.me(),
});
const userRole = user?.user_role || user?.role;
// Fetch pending vendors from database
const { data: pendingVendors = [] } = useQuery({
queryKey: ['pending-vendors'],
queryFn: () => base44.entities.Vendor.filter({ approval_status: 'pending' }, '-created_date'),
initialData: [],
});
// Fetch approved vendors
const { data: approvedVendors = [] } = useQuery({
queryKey: ['approved-vendors'],
queryFn: () => base44.entities.Vendor.list('-created_date'),
initialData: [],
});
// Fetch all vendor rates
const { data: allVendorRates = [] } = useQuery({
queryKey: ['all-vendor-rates'],
queryFn: () => base44.entities.VendorRate.list('-created_date'),
initialData: [],
});
// Approve vendor mutation
const approveVendorMutation = useMutation({
mutationFn: async (vendorId) => {
await base44.entities.Vendor.update(vendorId, {
approval_status: 'approved',
approved_date: new Date().toISOString(),
approved_by: user?.email,
is_active: true,
});
// Activate all vendor rates
const vendorRates = allVendorRates.filter(r => r.vendor_id === vendorId);
await Promise.all(
vendorRates.map(rate =>
base44.entities.VendorRate.update(rate.id, { is_active: true })
)
);
},
onSuccess: () => {
queryClient.invalidateQueries(['pending-vendors']);
queryClient.invalidateQueries(['approved-vendors']);
queryClient.invalidateQueries(['all-vendor-rates']);
toast({
title: "Vendor Approved",
description: "Vendor has been approved and activated",
});
setShowReviewModal(false);
setReviewingVendor(null);
},
});
// Reject vendor mutation
const rejectVendorMutation = useMutation({
mutationFn: async ({ vendorId, reason }) => {
await base44.entities.Vendor.update(vendorId, {
approval_status: 'suspended',
is_active: false,
notes: `Rejected: ${reason}`,
});
// Send rejection email
const vendor = [...pendingVendors, ...approvedVendors].find(v => v.id === vendorId);
if (vendor?.primary_contact_email) {
await base44.integrations.Core.SendEmail({
from_name: "KROW Platform",
to: vendor.primary_contact_email,
subject: "KROW Vendor Application - Additional Information Required",
body: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
<h2>Vendor Application Update</h2>
<p>Dear ${vendor.primary_contact_name || 'Vendor'},</p>
<p>Thank you for your interest in joining the KROW vendor network.</p>
<p>We've reviewed your application and need some additional information before we can proceed:</p>
<div style="background: #fef3c7; padding: 16px; border-radius: 8px; margin: 20px 0; border-left: 4px solid #f59e0b;">
<p style="margin: 0; color: #92400e;"><strong>Reason:</strong> ${reason}</p>
</div>
<p>Please address the above and resubmit your application.</p>
<p>Best regards,<br>KROW Procurement Team</p>
</div>
`
});
}
},
onSuccess: () => {
queryClient.invalidateQueries(['pending-vendors']);
queryClient.invalidateQueries(['approved-vendors']);
toast({
title: "Vendor Rejected",
description: "Vendor has been notified and can resubmit",
});
setShowReviewModal(false);
setShowRejectModal(false);
setReviewingVendor(null);
setRejectionReason("");
},
});
// Archive vendor mutation
const archiveVendorMutation = useMutation({
mutationFn: async (vendorId) => {
await base44.entities.Vendor.update(vendorId, {
approval_status: 'archived',
is_active: false,
});
},
onSuccess: () => {
queryClient.invalidateQueries(['pending-vendors']);
queryClient.invalidateQueries(['approved-vendors']);
toast({
title: "Vendor Archived",
description: "Vendor has been archived",
});
},
});
// Mock vendors for existing list
const mockApprovedVendors = [
{ id: 'mock-1001', vendorNumber: "VN-1001", name: "Legendary Event Staffing", region: "Bay Area", state: "California", specialty: "Full Service Events", software: "Building platform (KROW)", softwareType: "building", employees: 450, fillRate: 97, onTimeRate: 98, csat: 4.7, spend: "$230k", approval_status: "approved", legal_name: "Legendary Event Staffing Inc.", tax_id: "XX-XXXXXXX", business_type: "Corporation", primary_contact_name: "Jane Doe", primary_contact_email: "jane@legendary.com", w9_document: true, coi_document: true, coverage_regions: ["San Francisco", "Oakland", "San Jose"] },
{ id: 'mock-1002', vendorNumber: "VN-1002", name: "Instawork", region: "National", state: "Multiple", specialty: "On-Demand Gig Platform", software: "Full Platform", softwareType: "platform", employees: 5000, fillRate: 95, onTimeRate: 96, csat: 4.5, spend: "$215k", approval_status: "approved", legal_name: "Instawork Corp.", tax_id: "XX-XXXXXXX", business_type: "Corporation", primary_contact_name: "John Smith", primary_contact_email: "john@instawork.com", w9_document: true, coi_document: true, coverage_regions: ["New York", "Los Angeles", "Chicago"] },
{ id: 'mock-1003', vendorNumber: "VN-1003", name: "The Party Staff", region: "Bay Area", state: "California", specialty: "Event Staffing", software: "Building platform (KROW)", softwareType: "building", employees: 320, fillRate: 94, onTimeRate: 97, csat: 4.6, spend: "$180k", approval_status: "approved", legal_name: "The Party Staff LLC", tax_id: "XX-XXXXXXX", business_type: "LLC", primary_contact_name: "Sarah Johnson", primary_contact_email: "sarah@thepartystaff.com", w9_document: true, coi_document: true, coverage_regions: ["San Francisco", "Oakland"] },
{ id: 'mock-1004', vendorNumber: "VN-1004", name: "Elite Hospitality Staffing", region: "Bay Area", state: "California", specialty: "Hotels/Events", software: "Partial Tech", softwareType: "partial", employees: 280, fillRate: 92, onTimeRate: 95, csat: 4.4, spend: "$165k", approval_status: "approved", legal_name: "Elite Hospitality Staffing Inc.", tax_id: "XX-XXXXXXX", business_type: "Corporation", primary_contact_name: "Michael Chen", primary_contact_email: "michael@elitehospitality.com", w9_document: true, coi_document: true, coverage_regions: ["Oakland", "San Jose"] },
{ id: 'mock-1005', vendorNumber: "VN-1005", name: "Jeff Duerson Staffing", region: "Bay Area", state: "California", specialty: "Catering/Events", software: "Traditional", softwareType: "traditional", employees: 150, fillRate: 90, onTimeRate: 93, csat: 4.3, spend: "$140k", approval_status: "approved", legal_name: "Jeff Duerson Staffing LLC", tax_id: "XX-XXXXXXX", business_type: "LLC", primary_contact_name: "Jeff Duerson", primary_contact_email: "jeff@jdstaffing.com", w9_document: true, coi_document: true, coverage_regions: ["San Jose", "Palo Alto"] },
{ id: 'mock-1006', vendorNumber: "VN-1006", name: "Flagship Culinary Services", region: "Bay Area", state: "California", specialty: "Culinary", software: "Traditional", softwareType: "traditional", employees: 200, fillRate: 91, onTimeRate: 94, csat: 4.5, spend: "$155k", approval_status: "approved", legal_name: "Flagship Culinary Services Inc.", tax_id: "XX-XXXXXXX", business_type: "Corporation", primary_contact_name: "Amanda Rodriguez", primary_contact_email: "amanda@flagshipculinary.com", w9_document: true, coi_document: true, coverage_regions: ["Palo Alto", "San Francisco"] },
{ id: 'mock-1007', vendorNumber: "VN-1007", name: "LA Event Professionals", region: "Southern California", state: "California", specialty: "Event Staffing", software: "Building platform (KROW)", softwareType: "building", employees: 380, fillRate: 93, onTimeRate: 96, csat: 4.5, spend: "$190k", approval_status: "approved", legal_name: "LA Event Professionals LLC", tax_id: "XX-XXXXXXX", business_type: "LLC", primary_contact_name: "Carlos Martinez", primary_contact_email: "carlos@laeventpros.com", w9_document: true, coi_document: true, coverage_regions: ["Los Angeles", "Long Beach"] },
{ id: 'mock-1008', vendorNumber: "VN-1008", name: "Hollywood Hospitality", region: "Southern California", state: "California", specialty: "Full Service Events", software: "Full Platform", softwareType: "platform", employees: 420, fillRate: 94, onTimeRate: 97, csat: 4.6, spend: "$210k", approval_status: "approved", legal_name: "Hollywood Hospitality Inc.", tax_id: "XX-XXXXXXX", business_type: "Corporation", primary_contact_name: "Jessica Lee", primary_contact_email: "jessica@hollywoodhospitality.com", w9_document: true, coi_document: true, coverage_regions: ["Los Angeles", "Beverly Hills"] },
{ id: 'mock-1009', vendorNumber: "VN-1009", name: "San Diego Event Staff", region: "Southern California", state: "California", specialty: "Hospitality", software: "Partial Tech", softwareType: "partial", employees: 250, fillRate: 91, onTimeRate: 94, csat: 4.4, spend: "$145k", approval_status: "approved", legal_name: "San Diego Event Staff LLC", tax_id: "XX-XXXXXXX", business_type: "LLC", primary_contact_name: "David Kim", primary_contact_email: "david@sdeventstaff.com", w9_document: true, coi_document: true, coverage_regions: ["San Diego", "Chula Vista"] },
{ id: 'mock-1010', vendorNumber: "VN-1010", name: "OC Staffing Solutions", region: "Southern California", state: "California", specialty: "Event-based staffing", software: "Traditional", softwareType: "traditional", employees: 180, fillRate: 89, onTimeRate: 92, csat: 4.2, spend: "$130k", approval_status: "approved", legal_name: "OC Staffing Solutions Inc.", tax_id: "XX-XXXXXXX", business_type: "Corporation", primary_contact_name: "Emily Watson", primary_contact_email: "emily@ocstaffing.com", w9_document: true, coi_document: true, coverage_regions: ["Irvine", "Anaheim"] },
{ id: 'mock-1011', vendorNumber: "VN-1011", name: "NorCal Staffing", region: "Northern California", state: "California", specialty: "Event-based staffing", software: "Partial Tech", softwareType: "partial", employees: 220, fillRate: 90, onTimeRate: 93, csat: 4.3, spend: "$150k", approval_status: "approved", legal_name: "NorCal Staffing LLC", tax_id: "XX-XXXXXXX", business_type: "LLC", primary_contact_name: "Robert Brown", primary_contact_email: "robert@norcalstaffing.com", w9_document: true, coi_document: true, coverage_regions: ["Sacramento", "Stockton"] },
{ id: 'mock-1012', vendorNumber: "VN-1012", name: "Kitchen & Cater Staffing", region: "Northern California", state: "California", specialty: "Catering", software: "Traditional", softwareType: "traditional", employees: 160, fillRate: 88, onTimeRate: 91, csat: 4.1, spend: "$125k", approval_status: "approved", legal_name: "Kitchen & Cater Staffing Inc.", tax_id: "XX-XXXXXXX", business_type: "Corporation", primary_contact_name: "Lisa Chang", primary_contact_email: "lisa@kitchencater.com", w9_document: true, coi_document: true, coverage_regions: ["Sacramento", "Davis"] },
{ id: 'mock-1013', vendorNumber: "VN-1013", name: "NYC Event Masters", region: "East Coast", state: "New York", specialty: "Full Service Events", software: "Full Platform", softwareType: "platform", employees: 550, fillRate: 96, onTimeRate: 98, csat: 4.7, spend: "$240k", approval_status: "approved", legal_name: "NYC Event Masters Inc.", tax_id: "XX-XXXXXXX", business_type: "Corporation", primary_contact_name: "James Wilson", primary_contact_email: "james@nyceventmasters.com", w9_document: true, coi_document: true, coverage_regions: ["New York City", "Brooklyn", "Queens"] },
{ id: 'mock-1014', vendorNumber: "VN-1014", name: "Manhattan Staffing Group", region: "East Coast", state: "New York", specialty: "Hospitality", software: "Building platform (KROW)", softwareType: "building", employees: 410, fillRate: 94, onTimeRate: 96, csat: 4.5, spend: "$200k", approval_status: "approved", legal_name: "Manhattan Staffing Group LLC", tax_id: "XX-XXXXXXX", business_type: "LLC", primary_contact_name: "Rachel Green", primary_contact_email: "rachel@manhattanstaffing.com", w9_document: true, coi_document: true, coverage_regions: ["Manhattan", "Bronx"] },
{ id: 'mock-1015', vendorNumber: "VN-1015", name: "Brooklyn Event Pros", region: "East Coast", state: "New York", specialty: "Event Staffing", software: "Partial Tech", softwareType: "partial", employees: 290, fillRate: 92, onTimeRate: 95, csat: 4.4, spend: "$170k", approval_status: "approved", legal_name: "Brooklyn Event Pros Inc.", tax_id: "XX-XXXXXXX", business_type: "Corporation", primary_contact_name: "Daniel Park", primary_contact_email: "daniel@brooklyneventpros.com", w9_document: true, coi_document: true, coverage_regions: ["Brooklyn", "Queens"] },
{ id: 'mock-1016', vendorNumber: "VN-1016", name: "Upstate Event Services", region: "East Coast", state: "New York", specialty: "Regional Events", software: "Traditional", softwareType: "traditional", employees: 140, fillRate: 87, onTimeRate: 90, csat: 4.0, spend: "$115k", approval_status: "approved", legal_name: "Upstate Event Services LLC", tax_id: "XX-XXXXXXX", business_type: "LLC", primary_contact_name: "Karen Miller", primary_contact_email: "karen@upstateeventservices.com", w9_document: true, coi_document: true, coverage_regions: ["Buffalo", "Rochester"] },
{ id: 'mock-1017', vendorNumber: "VN-1017", name: "Lone Star Staffing", region: "South", state: "Texas", specialty: "Full Service Events", software: "Building platform (KROW)", softwareType: "building", employees: 370, fillRate: 93, onTimeRate: 96, csat: 4.5, spend: "$185k", approval_status: "approved", legal_name: "Lone Star Staffing Inc.", tax_id: "XX-XXXXXXX", business_type: "Corporation", primary_contact_name: "Austin Davis", primary_contact_email: "austin@lonestarstaffing.com", w9_document: true, coi_document: true, coverage_regions: ["Austin", "Round Rock"] },
{ id: 'mock-1018', vendorNumber: "VN-1018", name: "Houston Hospitality Hub", region: "South", state: "Texas", specialty: "Hospitality", software: "Full Platform", softwareType: "platform", employees: 460, fillRate: 95, onTimeRate: 97, csat: 4.6, spend: "$220k", approval_status: "approved", legal_name: "Houston Hospitality Hub LLC", tax_id: "XX-XXXXXXX", business_type: "LLC", primary_contact_name: "Maria Garcia", primary_contact_email: "maria@houstonhospitality.com", w9_document: true, coi_document: true, coverage_regions: ["Houston", "Galveston"] },
{ id: 'mock-1019', vendorNumber: "VN-1019", name: "Dallas Event Specialists", region: "South", state: "Texas", specialty: "Event Staffing", software: "Partial Tech", softwareType: "partial", employees: 340, fillRate: 92, onTimeRate: 95, csat: 4.4, spend: "$175k", approval_status: "approved", legal_name: "Dallas Event Specialists Inc.", tax_id: "XX-XXXXXXX", business_type: "Corporation", primary_contact_name: "Brandon White", primary_contact_email: "brandon@dallaseventspecialists.com", w9_document: true, coi_document: true, coverage_regions: ["Dallas", "Fort Worth"] },
{ id: 'mock-1020', vendorNumber: "VN-1020", name: "San Antonio Services", region: "South", state: "Texas", specialty: "Catering/Events", software: "Traditional", softwareType: "traditional", employees: 190, fillRate: 89, onTimeRate: 92, csat: 4.2, spend: "$135k", approval_status: "approved", legal_name: "San Antonio Services LLC", tax_id: "XX-XXXXXXX", business_type: "LLC", primary_contact_name: "Jennifer Lopez", primary_contact_email: "jennifer@sanantonioservices.com", w9_document: true, coi_document: true, coverage_regions: ["San Antonio"] },
{ id: 'mock-1021', vendorNumber: "VN-1021", name: "Chicago Event Staffing", region: "Midwest", state: "Illinois", specialty: "Full Service Events", software: "Full Platform", softwareType: "platform", employees: 490, fillRate: 95, onTimeRate: 97, csat: 4.6, spend: "$225k", approval_status: "approved", legal_name: "Chicago Event Staffing Inc.", tax_id: "XX-XXXXXXX", business_type: "Corporation", primary_contact_name: "Thomas Anderson", primary_contact_email: "thomas@chicagoeventstaffing.com", w9_document: true, coi_document: true, coverage_regions: ["Chicago", "Naperville"] },
{ id: 'mock-1022', vendorNumber: "VN-1022", name: "Windy City Hospitality", region: "Midwest", state: "Illinois", specialty: "Hotels/Events", software: "Building platform (KROW)", softwareType: "building", employees: 380, fillRate: 93, onTimeRate: 96, csat: 4.5, spend: "$190k", approval_status: "approved", legal_name: "Windy City Hospitality LLC", tax_id: "XX-XXXXXXX", business_type: "LLC", primary_contact_name: "Nicole Taylor", primary_contact_email: "nicole@windycityhospitality.com", w9_document: true, coi_document: true, coverage_regions: ["Chicago", "Evanston"] },
{ id: 'mock-1023', vendorNumber: "VN-1023", name: "Miami Event Solutions", region: "South", state: "Florida", specialty: "Full Service Events", software: "Full Platform", softwareType: "platform", employees: 440, fillRate: 94, onTimeRate: 96, csat: 4.5, spend: "$205k", approval_status: "approved", legal_name: "Miami Event Solutions Inc.", tax_id: "XX-XXXXXXX", business_type: "Corporation", primary_contact_name: "Carlos Hernandez", primary_contact_email: "carlos@miamievertsolutions.com", w9_document: true, coi_document: true, coverage_regions: ["Miami", "Fort Lauderdale"] },
{ id: 'mock-1024', vendorNumber: "VN-1024", name: "Orlando Hospitality Group", region: "South", state: "Florida", specialty: "Theme Park Events", software: "Building platform (KROW)", softwareType: "building", employees: 520, fillRate: 95, onTimeRate: 97, csat: 4.7, spend: "$235k", approval_status: "approved", legal_name: "Orlando Hospitality Group LLC", tax_id: "XX-XXXXXXX", business_type: "LLC", primary_contact_name: "Michelle Scott", primary_contact_email: "michelle@orlandohospitality.com", w9_document: true, coi_document: true, coverage_regions: ["Orlando", "Kissimmee"] },
{ id: 'mock-1025', vendorNumber: "VN-1025", name: "Tampa Bay Staffing", region: "South", state: "Florida", specialty: "Event Staffing", software: "Partial Tech", softwareType: "partial", employees: 310, fillRate: 91, onTimeRate: 94, csat: 4.3, spend: "$160k", approval_status: "approved", legal_name: "Tampa Bay Staffing Inc.", tax_id: "XX-XXXXXXX", business_type: "Corporation", primary_contact_name: "Kevin Moore", primary_contact_email: "kevin@tampabaystaffing.com", w9_document: true, coi_document: true, coverage_regions: ["Tampa", "St. Petersburg"] },
{ id: 'mock-1026', vendorNumber: "VN-1026", name: "Seattle Event Pros", region: "West", state: "Washington", specialty: "Tech Events", software: "Full Platform", softwareType: "platform", employees: 400, fillRate: 94, onTimeRate: 96, csat: 4.6, spend: "$200k", approval_status: "approved", legal_name: "Seattle Event Pros LLC", tax_id: "XX-XXXXXXX", business_type: "LLC", primary_contact_name: "Amy Chen", primary_contact_email: "amy@seattleeventpros.com", w9_document: true, coi_document: true, coverage_regions: ["Seattle", "Bellevue"] },
{ id: 'mock-1027', vendorNumber: "VN-1027", name: "Pacific Northwest Staffing", region: "West", state: "Washington", specialty: "Full Service Events", software: "Building platform (KROW)", softwareType: "building", employees: 350, fillRate: 93, onTimeRate: 95, csat: 4.5, spend: "$180k", approval_status: "approved", legal_name: "Pacific Northwest Staffing Inc.", tax_id: "XX-XXXXXXX", business_type: "Corporation", primary_contact_name: "Brian Lee", primary_contact_email: "brian@pnwstaffing.com", w9_document: true, coi_document: true, coverage_regions: ["Seattle", "Tacoma"] },
{ id: 'mock-1028', vendorNumber: "VN-1028", name: "Boston Event Services", region: "East Coast", state: "Massachusetts", specialty: "Corporate Events", software: "Full Platform", softwareType: "platform", employees: 370, fillRate: 93, onTimeRate: 96, csat: 4.5, spend: "$185k", approval_status: "approved", legal_name: "Boston Event Services LLC", tax_id: "XX-XXXXXXX", business_type: "LLC", primary_contact_name: "Patrick Sullivan", primary_contact_email: "patrick@bostoneventservices.com", w9_document: true, coi_document: true, coverage_regions: ["Boston", "Cambridge"] },
{ id: 'mock-1029', vendorNumber: "VN-1029", name: "New England Hospitality", region: "East Coast", state: "Massachusetts", specialty: "Full Service Events", software: "Building platform (KROW)", softwareType: "building", employees: 320, fillRate: 92, onTimeRate: 95, csat: 4.4, spend: "$170k", approval_status: "approved", legal_name: "New England Hospitality Inc.", tax_id: "XX-XXXXXXX", business_type: "Corporation", primary_contact_name: "Laura Murphy", primary_contact_email: "laura@nehospitality.com", w9_document: true, coi_document: true, coverage_regions: ["Boston", "Worcester"] },
{ id: 'mock-1030', vendorNumber: "VN-1030", name: "Atlanta Event Staffing", region: "South", state: "Georgia", specialty: "Full Service Events", software: "Full Platform", softwareType: "platform", employees: 420, fillRate: 94, onTimeRate: 96, csat: 4.6, spend: "$210k", approval_status: "approved", legal_name: "Atlanta Event Staffing LLC", tax_id: "XX-XXXXXXX", business_type: "LLC", primary_contact_name: "Marcus Johnson", primary_contact_email: "marcus@atlantaeventstaffing.com", w9_document: true, coi_document: true, coverage_regions: ["Atlanta", "Marietta"] },
{ id: 'mock-1031', vendorNumber: "VN-1031", name: "Peach State Hospitality", region: "South", state: "Georgia", specialty: "Hospitality", software: "Building platform (KROW)", softwareType: "building", employees: 340, fillRate: 92, onTimeRate: 95, csat: 4.4, spend: "$175k", approval_status: "approved", legal_name: "Peach State Hospitality Inc.", tax_id: "XX-XXXXXXX", business_type: "Corporation", primary_contact_name: "Stephanie Wright", primary_contact_email: "stephanie@peachstatehospitality.com", w9_document: true, coi_document: true, coverage_regions: ["Atlanta", "Savannah"] },
{ id: 'mock-1032', vendorNumber: "VN-1032", name: "Denver Event Solutions", region: "West", state: "Colorado", specialty: "Mountain Events", software: "Partial Tech", softwareType: "partial", employees: 280, fillRate: 91, onTimeRate: 94, csat: 4.3, spend: "$150k", approval_status: "approved", legal_name: "Denver Event Solutions LLC", tax_id: "XX-XXXXXXX", business_type: "LLC", primary_contact_name: "Jake Thompson", primary_contact_email: "jake@denvereventsolutions.com", w9_document: true, coi_document: true, coverage_regions: ["Denver", "Boulder"] },
{ id: 'mock-1033', vendorNumber: "VN-1033", name: "Rocky Mountain Staffing", region: "West", state: "Colorado", specialty: "Event Staffing", software: "Traditional", softwareType: "traditional", employees: 230, fillRate: 89, onTimeRate: 92, csat: 4.2, spend: "$140k", approval_status: "approved", legal_name: "Rocky Mountain Staffing Inc.", tax_id: "XX-XXXXXXX", business_type: "Corporation", primary_contact_name: "Hannah Davis", primary_contact_email: "hannah@rockymountainstaffing.com", w9_document: true, coi_document: true, coverage_regions: ["Denver", "Colorado Springs"] },
{ id: 'mock-1034', vendorNumber: "VN-1034", name: "Hospitality Staffing Solutions (HSS)", region: "National", state: "Multiple", specialty: "Hospitality", software: "Full Platform", softwareType: "platform", employees: 4200, fillRate: 96, onTimeRate: 98, csat: 4.7, spend: "$195k", approval_status: "approved", legal_name: "HSS Corporation", tax_id: "XX-XXXXXXX", business_type: "Corporation", primary_contact_name: "Richard Lawson", primary_contact_email: "richard@hss.com", w9_document: true, coi_document: true, coverage_regions: ["National Coverage"] },
{ id: 'mock-1035', vendorNumber: "VN-1035", name: "Bluecrew", region: "National", state: "Multiple", specialty: "W-2 Staffing Platform", software: "Full Platform", softwareType: "platform", employees: 3800, fillRate: 95, onTimeRate: 97, csat: 4.6, spend: "$185k", approval_status: "approved", legal_name: "Bluecrew Inc.", tax_id: "XX-XXXXXXX", business_type: "Corporation", primary_contact_name: "Chris Miller", primary_contact_email: "chris@bluecrew.com", w9_document: true, coi_document: true, coverage_regions: ["National Coverage"] },
{ id: 'mock-1036', vendorNumber: "VN-1036", name: "Qwick", region: "National", state: "Multiple", specialty: "Hospitality Platform", software: "Full Platform", softwareType: "platform", employees: 3500, fillRate: 94, onTimeRate: 96, csat: 4.5, spend: "$175k", approval_status: "approved", legal_name: "Qwick Inc.", tax_id: "XX-XXXXXXX", business_type: "Corporation", primary_contact_name: "Sarah Mitchell", primary_contact_email: "sarah@qwick.com", w9_document: true, coi_document: true, coverage_regions: ["National Coverage"] },
];
// Combine all vendors (mock + database approved + database pending)
const allVendorsMap = new Map();
mockApprovedVendors.forEach(vendor => allVendorsMap.set(vendor.id, vendor));
approvedVendors.forEach(vendor => {
if (!allVendorsMap.has(vendor.id)) {
allVendorsMap.set(vendor.id, {
...vendor,
name: vendor.legal_name,
vendorNumber: vendor.vendor_number,
});
}
});
const allVendors = Array.from(allVendorsMap.values());
// Calculate status counts
const statusCounts = {
all: allVendors.length,
approved: allVendors.filter(v => v.approval_status === "approved").length,
pending: pendingVendors.length,
suspended: allVendors.filter(v => v.approval_status === "suspended").length,
terminated: allVendors.filter(v => v.approval_status === "terminated").length,
archived: allVendors.filter(v => v.approval_status === "archived").length,
};
// Get unique values for filters
const uniqueRegions = [...new Set(allVendors.map(v => v.region).filter(Boolean))].sort();
const uniqueStates = [...new Set(allVendors.map(v => v.state).filter(Boolean))].sort();
const uniqueSpecialties = [...new Set(allVendors.map(v => v.specialty).filter(Boolean))].sort();
// Apply filters with proper logic
const filteredVendors = allVendors.filter(vendor => {
const matchesSearch = !searchTerm ||
vendor.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
vendor.vendorNumber?.toLowerCase().includes(searchTerm.toLowerCase()) ||
vendor.vendor_number?.toLowerCase().includes(searchTerm.toLowerCase()) ||
vendor.specialty?.toLowerCase().includes(searchTerm.toLowerCase());
const matchesRegion = regionFilter === "all" || vendor.region === regionFilter;
const matchesSpecialty = specialtyFilter === "all" || vendor.specialty === specialtyFilter;
const matchesState = stateFilter === "all" || vendor.state === stateFilter;
const matchesSoftware = softwareFilter === "all" || vendor.softwareType === softwareFilter;
const matchesStatus = statusFilter === "all" || vendor.approval_status === statusFilter;
return matchesSearch && matchesRegion && matchesSpecialty && matchesState && matchesSoftware && matchesStatus;
});
// Reset to page 1 when filters change
React.useEffect(() => {
setCurrentPage(1);
}, [searchTerm, regionFilter, stateFilter, specialtyFilter, softwareFilter, statusFilter]);
// Pagination
const totalPages = Math.ceil(filteredVendors.length / itemsPerPage);
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const paginatedVendors = filteredVendors.slice(startIndex, endIndex);
const getSoftwareBadge = (type, label) => {
if (type === "platform") {
return <Badge className="bg-green-100 text-green-700"> {label}</Badge>;
} else if (type === "building") {
return <Badge className="bg-blue-100 text-blue-700"> {label}</Badge>;
} else if (type === "partial") {
return <Badge className="bg-yellow-100 text-yellow-700"> {label}</Badge>;
} else {
return <Badge className="bg-gray-100 text-gray-700"> {label}</Badge>;
}
};
const handleEditVendor = (vendor) => {
setEditingVendor(vendor);
setShowEditModal(true);
};
const handleReviewVendor = (vendor) => {
setReviewingVendor(vendor);
setShowReviewModal(true);
};
const handleRejectClick = () => {
setShowRejectModal(true);
};
const getStatusColor = (status) => {
const colors = {
all: "from-slate-600 to-slate-700",
approved: "from-emerald-500 to-emerald-600",
pending: "from-amber-500 to-amber-600",
suspended: "from-orange-500 to-orange-600",
terminated: "from-red-500 to-red-600",
archived: "from-slate-400 to-slate-500"
};
return colors[status] || colors.all;
};
const getStatusIcon = (status) => {
const icons = {
all: Package,
approved: CheckCircle2,
pending: Clock,
suspended: XCircle,
terminated: XCircle,
archived: Archive
};
return icons[status] || Package;
};
const getStatusLabel = (status) => {
const labels = {
all: "All",
approved: "Active",
pending: "Review",
suspended: "Hold",
terminated: "Closed",
archived: "Archived"
};
return labels[status] || status;
};
return (
<div className="p-4 md:p-8 bg-slate-50 min-h-screen">
<div className="max-w-7xl mx-auto">
<PageHeader
title="Vendor Management"
subtitle={`${statusCounts.pending} pending • ${statusCounts.approved} approved`}
actions={
<div className="flex gap-2">
<Link to={createPageUrl("InviteVendor")}>
<Button className="bg-gradient-to-r from-green-600 to-emerald-600 hover:from-green-700 hover:to-emerald-700 text-white shadow-lg">
<Mail className="w-5 h-5 mr-2" />
Invite Vendor
</Button>
</Link>
<Link to={createPageUrl("SmartVendorOnboarding")}>
<Button className="bg-gradient-to-r from-[#0A39DF] to-[#1C323E] hover:from-[#0A39DF]/90 hover:to-[#1C323E]/90 text-white shadow-lg">
<Plus className="w-5 h-5 mr-2" />
Vendor Onboard
</Button>
</Link>
</div>
}
/>
{/* Status Cards */}
<div className="grid grid-cols-2 md:grid-cols-6 gap-4 mb-6">
{/* All Vendors */}
<Card
className={`cursor-pointer transition-all duration-200 border-2 ${
statusFilter === "all" ? "border-slate-600 shadow-lg scale-105" : "border-slate-200 hover:shadow-md hover:border-slate-300"
}`}
onClick={() => setStatusFilter("all")}
>
<CardContent className="p-6">
<div className="flex items-center justify-between mb-3">
<div className={`w-12 h-12 bg-gradient-to-br ${getStatusColor("all")} rounded-xl flex items-center justify-center`}>
<Package className="w-6 h-6 text-white" />
</div>
<Badge className="bg-slate-100 text-slate-700 text-xs">{getStatusLabel("all")}</Badge>
</div>
<div className="text-3xl font-bold text-slate-900 mb-1">{statusCounts.all}</div>
<p className="text-sm text-slate-500">Total Vendors</p>
</CardContent>
</Card>
{/* Approved */}
<Card
className={`cursor-pointer transition-all duration-200 border-2 ${
statusFilter === "approved" ? "border-emerald-600 shadow-lg scale-105" : "border-slate-200 hover:shadow-md hover:border-emerald-200"
}`}
onClick={() => setStatusFilter("approved")}
>
<CardContent className="p-6">
<div className="flex items-center justify-between mb-3">
<div className={`w-12 h-12 bg-gradient-to-br ${getStatusColor("approved")} rounded-xl flex items-center justify-center`}>
<CheckCircle2 className="w-6 h-6 text-white" />
</div>
<Badge className="bg-emerald-100 text-emerald-700 text-xs">{getStatusLabel("approved")}</Badge>
</div>
<div className="text-3xl font-bold text-emerald-700 mb-1">{statusCounts.approved}</div>
<p className="text-sm text-slate-500">Approved</p>
</CardContent>
</Card>
{/* Pending */}
<Card
className={`cursor-pointer transition-all duration-200 border-2 ${
statusFilter === "pending" ? "border-amber-600 shadow-lg scale-105" : "border-slate-200 hover:shadow-md hover:border-amber-200"
}`}
onClick={() => setStatusFilter("pending")}
>
<CardContent className="p-6">
<div className="flex items-center justify-between mb-3">
<div className={`w-12 h-12 bg-gradient-to-br ${getStatusColor("pending")} rounded-xl flex items-center justify-center`}>
<Clock className="w-6 h-6 text-white" />
</div>
<Badge className="bg-amber-100 text-amber-700 text-xs">{getStatusLabel("pending")}</Badge>
</div>
<div className="text-3xl font-bold text-amber-700 mb-1">{statusCounts.pending}</div>
<p className="text-sm text-slate-500">Pending</p>
</CardContent>
</Card>
{/* Suspended */}
<Card
className={`cursor-pointer transition-all duration-200 border-2 ${
statusFilter === "suspended" ? "border-orange-600 shadow-lg scale-105" : "border-slate-200 hover:shadow-md hover:border-orange-200"
}`}
onClick={() => setStatusFilter("suspended")}
>
<CardContent className="p-6">
<div className="flex items-center justify-between mb-3">
<div className={`w-12 h-12 bg-gradient-to-br ${getStatusColor("suspended")} rounded-xl flex items-center justify-center`}>
<XCircle className="w-6 h-6 text-white" />
</div>
<Badge className="bg-orange-100 text-orange-700 text-xs">{getStatusLabel("suspended")}</Badge>
</div>
<div className="text-3xl font-bold text-orange-700 mb-1">{statusCounts.suspended}</div>
<p className="text-sm text-slate-500">Suspended</p>
</CardContent>
</Card>
{/* Terminated */}
<Card
className={`cursor-pointer transition-all duration-200 border-2 ${
statusFilter === "terminated" ? "border-red-600 shadow-lg scale-105" : "border-slate-200 hover:shadow-md hover:border-red-200"
}`}
onClick={() => setStatusFilter("terminated")}
>
<CardContent className="p-6">
<div className="flex items-center justify-between mb-3">
<div className={`w-12 h-12 bg-gradient-to-br ${getStatusColor("terminated")} rounded-xl flex items-center justify-center`}>
<XCircle className="w-6 h-6 text-white" />
</div>
<Badge className="bg-red-100 text-red-700 text-xs">{getStatusLabel("terminated")}</Badge>
</div>
<div className="text-3xl font-bold text-red-700 mb-1">{statusCounts.terminated}</div>
<p className="text-sm text-slate-500">Terminated</p>
</CardContent>
</Card>
{/* Archived */}
<Card
className={`cursor-pointer transition-all duration-200 border-2 ${
statusFilter === "archived" ? "border-slate-600 shadow-lg scale-105" : "border-slate-200 hover:shadow-md hover:border-slate-300"
}`}
onClick={() => setStatusFilter("archived")}
>
<CardContent className="p-6">
<div className="flex items-center justify-between mb-3">
<div className={`w-12 h-12 bg-gradient-to-br ${getStatusColor("archived")} rounded-xl flex items-center justify-center`}>
<Archive className="w-6 h-6 text-white" />
</div>
<Badge className="bg-slate-100 text-slate-700 text-xs">{getStatusLabel("archived")}</Badge>
</div>
<div className="text-3xl font-bold text-slate-700 mb-1">{statusCounts.archived}</div>
<p className="text-sm text-slate-500">Archived</p>
</CardContent>
</Card>
</div>
{/* Search and Filters */}
<Card className="mb-6 border-slate-200">
<CardContent className="p-4">
<div className="space-y-4">
<div className="flex gap-4 items-center">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-slate-400" />
<Input
placeholder="Search by vendor name, number, or specialty..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
{/* View Mode Toggle */}
<div className="flex items-center gap-1 bg-slate-100 p-1 rounded-lg">
<Button
size="sm"
variant={viewMode === "list" ? "default" : "ghost"}
onClick={() => setViewMode("list")}
className={viewMode === "list" ? "bg-[#0A39DF] hover:bg-[#0A39DF]/90" : "hover:bg-white"}
>
<ListIcon className="w-4 h-4" />
</Button>
<Button
size="sm"
variant={viewMode === "grid" ? "default" : "ghost"}
onClick={() => setViewMode("grid")}
className={viewMode === "grid" ? "bg-[#0A39DF] hover:bg-[#0A39DF]/90" : "hover:bg-white"}
>
<LayoutGrid className="w-4 h-4" />
</Button>
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
<div>
<Label className="text-xs text-slate-600 mb-1 block">Region</Label>
<Select value={regionFilter} onValueChange={setRegionFilter}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Regions</SelectItem>
{uniqueRegions.map(region => (
<SelectItem key={region} value={region}>{region}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label className="text-xs text-slate-600 mb-1 block">State</Label>
<Select value={stateFilter} onValueChange={setStateFilter}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All States</SelectItem>
{uniqueStates.map(state => (
<SelectItem key={state} value={state}>{state}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label className="text-xs text-slate-600 mb-1 block">Specialty</Label>
<Select value={specialtyFilter} onValueChange={setSpecialtyFilter}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Specialties</SelectItem>
{uniqueSpecialties.map(specialty => (
<SelectItem key={specialty} value={specialty}>{specialty}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label className="text-xs text-slate-600 mb-1 block">Software</Label>
<Select value={softwareFilter} onValueChange={setSoftwareFilter}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Software</SelectItem>
<SelectItem value="platform">Full Platform</SelectItem>
<SelectItem value="building">Building Platform</SelectItem>
<SelectItem value="partial">Partial Tech</SelectItem>
<SelectItem value="traditional">Traditional</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label className="text-xs text-slate-600 mb-1 block">Per Page</Label>
<Select value={itemsPerPage.toString()} onValueChange={(value) => setItemsPerPage(Number(value))}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="10">10 per page</SelectItem>
<SelectItem value="25">25 per page</SelectItem>
<SelectItem value="50">50 per page</SelectItem>
<SelectItem value="100">100 per page</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* Active Filters Display */}
{(searchTerm || regionFilter !== "all" || stateFilter !== "all" || specialtyFilter !== "all" || softwareFilter !== "all" || statusFilter !== "all") && (
<div className="flex flex-wrap gap-2 pt-2 border-t border-slate-200">
<span className="text-sm text-slate-600 font-medium">Active Filters:</span>
{searchTerm && (
<Badge variant="outline" className="bg-blue-50 text-blue-700 border-blue-200">
Search: "{searchTerm}"
<button onClick={() => setSearchTerm("")} className="ml-2 hover:text-blue-900">×</button>
</Badge>
)}
{statusFilter !== "all" && (
<Badge variant="outline" className="bg-emerald-50 text-emerald-700 border-emerald-200">
Status: {statusFilter}
<button onClick={() => setStatusFilter("all")} className="ml-2 hover:text-emerald-900">×</button>
</Badge>
)}
{regionFilter !== "all" && (
<Badge variant="outline" className="bg-purple-50 text-purple-700 border-purple-200">
Region: {regionFilter}
<button onClick={() => setRegionFilter("all")} className="ml-2 hover:text-purple-900">×</button>
</Badge>
)}
{stateFilter !== "all" && (
<Badge variant="outline" className="bg-pink-50 text-pink-700 border-pink-200">
State: {stateFilter}
<button onClick={() => setStateFilter("all")} className="ml-2 hover:text-pink-900">×</button>
</Badge>
)}
{specialtyFilter !== "all" && (
<Badge variant="outline" className="bg-indigo-50 text-indigo-700 border-indigo-200">
Specialty: {specialtyFilter}
<button onClick={() => setSpecialtyFilter("all")} className="ml-2 hover:text-indigo-900">×</button>
</Badge>
)}
{softwareFilter !== "all" && (
<Badge variant="outline" className="bg-cyan-50 text-cyan-700 border-cyan-200">
Software: {softwareFilter}
<button onClick={() => setSoftwareFilter("all")} className="ml-2 hover:text-cyan-900">×</button>
</Badge>
)}
<Button
variant="ghost"
size="sm"
onClick={() => {
setSearchTerm("");
setRegionFilter("all");
setStateFilter("all");
setSpecialtyFilter("all");
setSoftwareFilter("all");
setStatusFilter("all");
}}
className="text-red-600 hover:text-red-700 hover:bg-red-50 h-6"
>
Clear All
</Button>
</div>
)}
</div>
</CardContent>
</Card>
{/* Vendor Directory - Grid or List View */}
{viewMode === "grid" ? (
// Grid View
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{paginatedVendors.map((vendor, idx) => (
<Card key={vendor.id || idx} className={`border-slate-200 hover:shadow-lg transition-shadow ${vendor.approval_status === 'pending' ? 'border-2 border-amber-200 bg-amber-50/30' : ''}`}>
<CardContent className="p-6">
<div className="flex items-start gap-4 mb-4">
<div className={`w-12 h-12 bg-gradient-to-br ${vendor.approval_status === 'pending' ? 'from-amber-500 to-amber-600' : 'from-[#0A39DF] to-[#1C323E]'} rounded-lg flex items-center justify-center`}>
{vendor.approval_status === 'pending' ? (
<Clock className="w-6 h-6 text-white" />
) : (
<Building2 className="w-6 h-6 text-white" />
)}
</div>
<div className="flex-1 min-w-0">
<h3 className="font-bold text-[#1C323E] truncate">{vendor.name || vendor.legal_name}</h3>
<p className="text-xs text-slate-500">{vendor.vendorNumber || vendor.vendor_number}</p>
{vendor.approval_status === 'pending' && (
<Badge className="bg-amber-100 text-amber-700 text-xs mt-1">Pending Review</Badge>
)}
</div>
</div>
<div className="space-y-2 mb-4 text-sm">
<div className="flex justify-between">
<span className="text-slate-500">Region:</span>
<span className="font-medium">{vendor.region || '—'}</span>
</div>
<div className="flex justify-between">
<span className="text-slate-500">Specialty:</span>
<span className="font-medium truncate ml-2">{vendor.specialty || '—'}</span>
</div>
<div className="flex justify-between">
<span className="text-slate-500">Staff:</span>
<span className="font-medium">{vendor.employees ? vendor.employees.toLocaleString() : 'N/A'}</span>
</div>
</div>
<div className="flex gap-2">
{vendor.approval_status === 'pending' ? (
<Button
size="sm"
onClick={() => handleReviewVendor(vendor)}
className="flex-1 bg-amber-600 hover:bg-amber-700 text-white"
>
<Eye className="w-4 h-4 mr-1" />
Review
</Button>
) : (
<>
<Button
variant="outline"
size="sm"
className="flex-1"
onClick={() => handleEditVendor(vendor)}
>
<Edit className="w-4 h-4 mr-1" />
Edit
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm">
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => archiveVendorMutation.mutate(vendor.id)}>
<Archive className="w-4 h-4 mr-2" />
Archive
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</>
)}
</div>
</CardContent>
</Card>
))}
</div>
) : (
// List View (Table)
<Card className="border-slate-200 shadow-lg">
<CardContent className="p-0">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-slate-50 border-b-2 border-slate-200">
<tr>
<th className="text-left py-4 px-4 font-semibold text-sm text-slate-700">Vendor Name</th>
<th className="text-left py-4 px-4 font-semibold text-sm text-slate-700">Region</th>
<th className="text-left py-4 px-4 font-semibold text-sm text-slate-700">State</th>
<th className="text-left py-4 px-4 font-semibold text-sm text-slate-700">Specialty</th>
<th className="text-center py-4 px-4 font-semibold text-sm text-slate-700">Total Staff</th>
<th className="text-center py-4 px-4 font-semibold text-sm text-slate-700">Software/Platform</th>
<th className="text-center py-4 px-4 font-semibold text-sm text-slate-700">Actions</th>
</tr>
</thead>
<tbody>
{paginatedVendors.length > 0 ? (
paginatedVendors.map((vendor, idx) => (
<tr key={vendor.id || idx} className={`border-b border-slate-100 hover:bg-slate-50 transition-colors ${vendor.approval_status === 'pending' ? 'bg-amber-50/30' : ''}`}>
<td className="py-4 px-4">
<VendorScoreHoverCard vendor={vendor}>
<div
className="flex items-center gap-3 cursor-pointer"
onClick={() => {
if (vendor.approval_status === 'pending') {
handleReviewVendor(vendor);
} else {
handleEditVendor(vendor);
}
}}
>
<div className={`w-10 h-10 bg-gradient-to-br ${vendor.approval_status === 'pending' ? 'from-amber-500 to-amber-600' : 'from-[#0A39DF] to-[#1C323E]'} rounded-lg flex items-center justify-center`}>
{vendor.approval_status === 'pending' ? (
<Clock className="w-5 h-5 text-white" />
) : (
<Building2 className="w-5 h-5 text-white" />
)}
</div>
<div>
<p className="font-semibold text-[#1C323E] hover:text-[#0A39DF] flex items-center gap-2">
{vendor.name || vendor.legal_name}
{vendor.approval_status === 'pending' && (
<Badge className="bg-amber-100 text-amber-700 text-xs">Pending Review</Badge>
)}
</p>
</div>
</div>
</VendorScoreHoverCard>
</td>
<td className="py-4 px-4 text-sm text-slate-700">{vendor.region || '—'}</td>
<td className="py-4 px-4 text-sm text-slate-700">{vendor.state || '—'}</td>
<td className="py-4 px-4 text-sm text-slate-700">{vendor.specialty || '—'}</td>
<td className="py-4 px-4 text-center">
<Badge variant="outline" className="font-semibold text-[#0A39DF] border-[#0A39DF]">
<Users className="w-3 h-3 mr-1" />
{vendor.employees ? vendor.employees.toLocaleString() : 'N/A'}
</Badge>
</td>
<td className="py-4 px-4 text-center">
{vendor.software ? getSoftwareBadge(vendor.softwareType, vendor.software) : '—'}
</td>
<td className="py-4 px-4">
<div className="flex items-center justify-center gap-2">
{vendor.approval_status === 'pending' ? (
<Button
size="sm"
onClick={() => handleReviewVendor(vendor)}
className="bg-amber-600 hover:bg-amber-700 text-white"
>
<Eye className="w-4 h-4 mr-1" />
Review
</Button>
) : (
<>
<Link to={createPageUrl("VendorRateCard")}>
<Button variant="ghost" size="sm" className="text-[#0A39DF] hover:bg-[#0A39DF]/10">
<DollarSign className="w-4 h-4 mr-1" />
Rates
</Button>
</Link>
<Button
variant="ghost"
size="sm"
className="text-slate-600 hover:bg-slate-100"
onClick={() => handleEditVendor(vendor)}
>
<Edit className="w-4 h-4 mr-1" />
Edit
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm">
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => archiveVendorMutation.mutate(vendor.id)}>
<Archive className="w-4 h-4 mr-2" />
Archive
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</>
)}
</div>
</td>
</tr>
))
) : (
<tr>
<td colSpan="7" className="py-12 text-center">
<Building2 className="w-12 h-12 mx-auto text-slate-300 mb-3" />
<p className="text-slate-600 font-medium">No vendors found</p>
<p className="text-sm text-slate-500 mt-1">Try adjusting your search or filters</p>
</td>
</tr>
)}
</tbody>
</table>
</div>
{/* Pagination */}
{totalPages > 1 && (
<div className="flex items-center justify-between p-4 border-t border-slate-200 bg-slate-50">
<div className="text-sm text-slate-600">
Page {currentPage} of {totalPages}
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
disabled={currentPage === 1}
>
<ChevronLeft className="w-4 h-4 mr-1" />
Previous
</Button>
<div className="flex gap-1">
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
let pageNum;
if (totalPages <= 5) {
pageNum = i + 1;
} else if (currentPage <= 3) {
pageNum = i + 1;
} else if (currentPage >= totalPages - 2) {
pageNum = totalPages - 4 + i;
} else {
pageNum = currentPage - 2 + i;
}
return (
<Button
key={pageNum}
variant={currentPage === pageNum ? "default" : "outline"}
size="sm"
onClick={() => setCurrentPage(pageNum)}
className={currentPage === pageNum ? "bg-[#0A39DF] hover:bg-[#0A39DF]/90" : ""}
>
{pageNum}
</Button>
);
})}
</div>
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}
disabled={currentPage === totalPages}
>
Next
<ChevronRight className="w-4 h-4 ml-1" />
</Button>
</div>
</div>
)}
</CardContent>
</Card>
)}
</div>
{/* Edit Vendor Modal */}
<VendorDetailModal
vendor={editingVendor}
open={showEditModal}
onClose={() => {
setShowEditModal(false);
setEditingVendor(null);
}}
onEdit={(vendor) => {}}
/>
{/* Quick Reject Modal (for reject button in header) */}
<Dialog open={showRejectModal} onOpenChange={setShowRejectModal}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Reject Pending Vendors</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<p className="text-sm text-slate-600">
You have {pendingVendors.length} pending vendor{pendingVendors.length !== 1 ? 's' : ''}. Select vendors to reject:
</p>
<div className="max-h-60 overflow-y-auto space-y-2">
{pendingVendors.map((vendor) => (
<Card key={vendor.id} className="p-3 cursor-pointer hover:bg-slate-50" onClick={() => handleReviewVendor(vendor)}>
<div className="flex items-center justify-between">
<div>
<p className="font-medium">{vendor.legal_name}</p>
<p className="text-xs text-slate-500">{vendor.vendor_number}</p>
</div>
<Button size="sm" variant="outline">Review</Button>
</div>
</Card>
))}
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowRejectModal(false)}>
Cancel
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Review & Approve Modal */}
<Dialog open={showReviewModal} onOpenChange={setShowReviewModal}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="text-2xl">Review Vendor Application</DialogTitle>
</DialogHeader>
{reviewingVendor && (
<div className="space-y-6">
{/* Vendor Info */}
<Card className="bg-slate-50">
<CardContent className="p-4">
<h3 className="font-semibold text-lg mb-3">{reviewingVendor.legal_name || reviewingVendor.name}</h3>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-slate-500">Vendor Number:</span>
<span className="ml-2 font-medium">{reviewingVendor.vendor_number || reviewingVendor.vendorNumber}</span>
</div>
<div>
<span className="text-slate-500">Business Type:</span>
<span className="ml-2 font-medium">{reviewingVendor.business_type || 'N/A'}</span>
</div>
<div>
<span className="text-slate-500">Tax ID:</span>
<span className="ml-2 font-medium">{reviewingVendor.tax_id || 'N/A'}</span>
</div>
<div>
<span className="text-slate-500">Contact:</span>
<span className="ml-2 font-medium">{reviewingVendor.primary_contact_name || 'N/A'}</span>
</div>
</div>
</CardContent>
</Card>
{/* Documents */}
<div>
<h4 className="font-semibold mb-2">Documents</h4>
<div className="grid grid-cols-2 gap-3">
{reviewingVendor.w9_document ? (
<Card>
<CardContent className="p-3">
<div className="flex items-center gap-2">
<CheckCircle2 className="w-4 h-4 text-green-600" />
<span className="text-sm font-medium">W-9 Form</span>
</div>
</CardContent>
</Card>
) : (
<Card>
<CardContent className="p-3">
<div className="flex items-center gap-2 text-slate-500">
<XCircle className="w-4 h-4 text-red-500" />
<span className="text-sm font-medium">W-9 Form (Missing)</span>
</div>
</CardContent>
</Card>
)}
{reviewingVendor.coi_document ? (
<Card>
<CardContent className="p-3">
<div className="flex items-center gap-2">
<CheckCircle2 className="w-4 h-4 text-green-600" />
<span className="text-sm font-medium">COI</span>
</div>
</CardContent>
</Card>
) : (
<Card>
<CardContent className="p-3">
<div className="flex items-center gap-2 text-slate-500">
<XCircle className="w-4 h-4 text-red-500" />
<span className="text-sm font-medium">COI (Missing)</span>
</div>
</CardContent>
</Card>
)}
</div>
</div>
{/* Service Coverage */}
<div>
<h4 className="font-semibold mb-2">Service Coverage</h4>
{reviewingVendor.coverage_regions && reviewingVendor.coverage_regions.length > 0 ? (
<div className="flex flex-wrap gap-2">
{reviewingVendor.coverage_regions.map((city, i) => (
<Badge key={i} variant="outline">{city}</Badge>
))}
</div>
) : (
<p className="text-sm text-slate-500">No coverage regions specified.</p>
)}
</div>
{/* Rate Proposals */}
<div>
<h4 className="font-semibold mb-2">Rate Proposals ({allVendorRates.filter(r => r.vendor_id === reviewingVendor.id).length})</h4>
<div className="space-y-2 max-h-60 overflow-y-auto pr-2">
{allVendorRates.filter(r => r.vendor_id === reviewingVendor.id).length > 0 ? (
allVendorRates.filter(r => r.vendor_id === reviewingVendor.id).map((rate, i) => (
<Card key={i}>
<CardContent className="p-3">
<div className="flex items-center justify-between">
<div>
<span className="font-medium">{rate.role_name}</span>
<span className="text-xs text-slate-500 ml-2">{rate.category}</span>
</div>
<div className="flex items-center gap-4 text-sm">
<span className="text-slate-600">Pay: <strong className="text-green-600">${rate.employee_wage}</strong></span>
<span className="text-slate-600">Markup: <strong className="text-blue-600">{rate.markup_percentage}%</strong></span>
<span className="text-slate-600">Bill: <strong className="text-[#0A39DF]">${rate.client_rate}</strong></span>
</div>
</div>
</CardContent>
</Card>
))
) : (
<p className="text-sm text-slate-500">No rate proposals submitted yet.</p>
)}
</div>
</div>
{/* Rejection Reason (optional) */}
<div>
<Label htmlFor="rejection_reason">Rejection Reason (Optional)</Label>
<p className="text-xs text-slate-500 mb-2">Provide feedback to help the vendor improve their application</p>
<Textarea
id="rejection_reason"
value={rejectionReason}
onChange={(e) => setRejectionReason(e.target.value)}
placeholder="e.g., Missing insurance documents, incomplete rate card, coverage area doesn't match our needs..."
className="mt-2"
rows={3}
/>
</div>
</div>
)}
<DialogFooter>
<Button
variant="outline"
onClick={() => {
setShowReviewModal(false);
setReviewingVendor(null);
setRejectionReason("");
}}
>
Cancel
</Button>
<Button
variant="destructive"
onClick={() => {
const reason = rejectionReason.trim() || "No specific reason provided";
rejectVendorMutation.mutate({
vendorId: reviewingVendor.id,
reason: reason
});
}}
disabled={rejectVendorMutation.isPending || !reviewingVendor}
>
{rejectVendorMutation.isPending ? "Rejecting..." : (
<>
<XCircle className="w-4 h-4 mr-2" />
Reject
</>
)}
</Button>
<Button
onClick={() => approveVendorMutation.mutate(reviewingVendor.id)}
disabled={approveVendorMutation.isPending || !reviewingVendor}
className="bg-green-600 hover:bg-green-700"
>
{approveVendorMutation.isPending ? "Approving..." : (
<>
<CheckCircle2 className="w-4 h-4 mr-2" />
Approve & Activate
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}