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
2889 lines
143 KiB
JavaScript
2889 lines
143 KiB
JavaScript
|
||
import React, { useState, useEffect } from "react";
|
||
import { base44 } from "@/api/base44Client";
|
||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||
import { useNavigate } from "react-router-dom";
|
||
import { createPageUrl } from "@/utils";
|
||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Input } from "@/components/ui/input";
|
||
import { Label } from "@/components/ui/label";
|
||
import { Textarea } from "@/components/ui/textarea";
|
||
import { Badge } from "@/components/ui/badge";
|
||
import { Progress } from "@/components/ui/progress";
|
||
import {
|
||
Building2, FileText, MapPin, DollarSign, Sparkles, Upload,
|
||
CheckCircle2, Loader2, AlertCircle, ArrowRight, ArrowLeft,
|
||
Shield, TrendingUp, Users, Briefcase, Mail, Phone, Hash,
|
||
Search, Calendar, Plus, Trash2, Check, Target
|
||
} from "lucide-react";
|
||
import { useToast } from "@/components/ui/use-toast";
|
||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||
import { Checkbox } from "@/components/ui/checkbox";
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
DialogDescription,
|
||
} from "@/components/ui/dialog";
|
||
import DragDropFileUpload from "@/components/common/DragDropFileUpload";
|
||
import DocumentViewer from "@/components/vendor/DocumentViewer";
|
||
|
||
// Google Places Autocomplete Component
|
||
const GoogleAddressInput = ({ value, onChange, placeholder, label, required }) => {
|
||
const [inputValue, setInputValue] = useState(value || "");
|
||
const [suggestions, setSuggestions] = useState([]);
|
||
const [showSuggestions, setShowSuggestions] = useState(false);
|
||
const inputRef = React.useRef(null);
|
||
|
||
useEffect(() => {
|
||
setInputValue(value || "");
|
||
}, [value]);
|
||
|
||
const handleAddressSearch = async (searchText) => {
|
||
setInputValue(searchText);
|
||
|
||
if (searchText.length < 3) {
|
||
setSuggestions([]);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// Use Google Places API via integration
|
||
const response = await base44.integrations.Core.InvokeLLM({
|
||
prompt: `Extract and return ONLY a valid JSON array of 5 US address suggestions for: "${searchText}". Format each as: {"address": "full formatted address", "city": "city", "state": "state", "zip": "zip"}. Return ONLY the JSON array, no other text.`,
|
||
response_json_schema: {
|
||
type: "object",
|
||
properties: {
|
||
suggestions: {
|
||
type: "array",
|
||
items: {
|
||
type: "object",
|
||
properties: {
|
||
address: { type: "string" },
|
||
city: { type: "string" },
|
||
state: { type: "string" },
|
||
zip: { type: "string" }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
if (response?.suggestions) {
|
||
setSuggestions(response.suggestions);
|
||
setShowSuggestions(true);
|
||
}
|
||
} catch (error) {
|
||
console.error("Address search error:", error);
|
||
}
|
||
};
|
||
|
||
const handleSelectAddress = (suggestion) => {
|
||
const fullAddress = `${suggestion.address}, ${suggestion.city}, ${suggestion.state} ${suggestion.zip}`;
|
||
setInputValue(fullAddress);
|
||
onChange(fullAddress);
|
||
setSuggestions([]);
|
||
setShowSuggestions(false);
|
||
};
|
||
|
||
return (
|
||
<div className="relative">
|
||
<Label htmlFor={label}>
|
||
{label} {required && <span className="text-red-500">*</span>}
|
||
</Label>
|
||
<div className="relative mt-2">
|
||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||
<Input
|
||
ref={inputRef}
|
||
value={inputValue}
|
||
onChange={(e) => {
|
||
handleAddressSearch(e.target.value);
|
||
}}
|
||
onBlur={() => {
|
||
// Delay to allow click on suggestion
|
||
setTimeout(() => setShowSuggestions(false), 200);
|
||
}}
|
||
onFocus={() => {
|
||
if (suggestions.length > 0) setShowSuggestions(true);
|
||
}}
|
||
placeholder={placeholder}
|
||
className="pl-10"
|
||
/>
|
||
</div>
|
||
|
||
{showSuggestions && suggestions.length > 0 && (
|
||
<div className="absolute z-50 w-full mt-1 bg-white border border-slate-200 rounded-lg shadow-lg max-h-60 overflow-y-auto">
|
||
{suggestions.map((suggestion, idx) => (
|
||
<button
|
||
key={idx}
|
||
type="button"
|
||
onClick={() => handleSelectAddress(suggestion)}
|
||
className="w-full text-left px-4 py-3 hover:bg-slate-50 border-b border-slate-100 last:border-0 transition-colors"
|
||
>
|
||
<div className="flex items-start gap-2">
|
||
<MapPin className="w-4 h-4 text-slate-400 mt-0.5 flex-shrink-0" />
|
||
<div className="flex-1 min-w-0">
|
||
<p className="text-sm font-medium text-slate-900 truncate">{suggestion.address}</p>
|
||
<p className="text-xs text-slate-500">{suggestion.city}, {suggestion.state} {suggestion.zip}</p>
|
||
</div>
|
||
</div>
|
||
</button>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default function SmartVendorOnboarding() {
|
||
const { toast } = useToast();
|
||
const navigate = useNavigate();
|
||
|
||
const [currentStep, setCurrentStep] = useState(0); // Changed from 1 to 0 for welcome screen
|
||
const [generatingRates, setGeneratingRates] = useState(false);
|
||
|
||
// Function to detect minimum wage based on business address
|
||
const getMinimumWageFromAddress = (address) => {
|
||
if (!address) return 18; // Default fallback
|
||
|
||
const addr = address.toLowerCase();
|
||
|
||
// State-specific minimum wages (as of 2024)
|
||
if (addr.includes('california') || addr.includes(' ca ') || addr.includes('ca,')) return 16.00;
|
||
if (addr.includes('washington') || addr.includes(' wa ')) return 16.28;
|
||
if (addr.includes('massachusetts') || addr.includes(' ma ')) return 15.00;
|
||
if (addr.includes('new york') || addr.includes(' ny ')) return 15.00;
|
||
if (addr.includes('connecticut') || addr.includes(' ct ')) return 15.00;
|
||
if (addr.includes('arizona') || addr.includes(' az ')) return 14.35;
|
||
if (addr.includes('colorado') || addr.includes(' co ')) return 14.42;
|
||
if (addr.includes('maine') || addr.includes(' me ')) return 14.15;
|
||
if (addr.includes('oregon') || addr.includes(' or ')) return 14.20;
|
||
if (addr.includes('vermont') || addr.includes(' vt ')) return 13.67;
|
||
if (addr.includes('illinois') || addr.includes(' il ')) return 14.00;
|
||
if (addr.includes('rhode island') || addr.includes(' ri ')) return 14.00;
|
||
if (addr.includes('new jersey') || addr.includes(' nj ')) return 15.13;
|
||
if (addr.includes('florida') || addr.includes(' fl ')) return 12.00;
|
||
if (addr.includes('nevada') || addr.includes(' nv ')) return 12.00;
|
||
if (addr.includes('hawaii') || addr.includes(' hi ')) return 14.00;
|
||
if (addr.includes('texas') || addr.includes(' tx ')) return 7.25;
|
||
if (addr.includes('georgia') || addr.includes(' ga ')) return 7.25;
|
||
if (addr.includes('pennsylvania') || addr.includes(' pa ')) return 7.25;
|
||
if (addr.includes('ohio') || addr.includes(' oh ')) return 10.45;
|
||
if (addr.includes('michigan') || addr.includes(' mi ')) return 10.33;
|
||
if (addr.includes('virginia') || addr.includes(' va ')) return 12.00;
|
||
if (addr.includes('north carolina') || addr.includes(' nc ')) return 7.25;
|
||
if (addr.includes('tennessee') || addr.includes(' tn ')) return 7.25;
|
||
|
||
return 15.00; // Default minimum wage for unspecified states
|
||
};
|
||
|
||
const [formData, setFormData] = useState({
|
||
// NEW: NDA Step (Step 1)
|
||
nda_acknowledged: false,
|
||
nda_signed_by: "",
|
||
nda_signature_date: "",
|
||
nda_signature_time: "",
|
||
nda_signature_image: "", // NEW: Store the signature image data
|
||
|
||
// Contract Data (now Step 2)
|
||
contract_review_notes: "",
|
||
contract_acknowledged: false,
|
||
contract_va_fee_acknowledged: false,
|
||
contract_review_time: 0,
|
||
|
||
// Business Identity (now Step 3)
|
||
legal_name: "",
|
||
dba: "",
|
||
tax_id: "",
|
||
business_type: "",
|
||
primary_contact_name: "",
|
||
primary_contact_email: "",
|
||
primary_contact_phone: "",
|
||
billing_address: "",
|
||
service_address: "",
|
||
same_as_billing: false, // This now means "billing address is same as service address"
|
||
total_employees: "",
|
||
has_software: "",
|
||
software_name: "",
|
||
software_type: "",
|
||
|
||
// Documents (now Step 4)
|
||
w9_file: null,
|
||
coi_file: null,
|
||
sos_file: null,
|
||
w9_url: "",
|
||
coi_url: "",
|
||
sos_url: "",
|
||
background_check_attestation: false,
|
||
i9_compliance_attestation: false,
|
||
legal_compliance_attestation: false,
|
||
|
||
// Service Coverage (now Step 5)
|
||
coverage_regions: [],
|
||
selected_states: [],
|
||
selected_cities: {},
|
||
|
||
// Rate Proposals (now Step 6)
|
||
rate_proposals: [],
|
||
default_location: ""
|
||
});
|
||
|
||
const [uploadingDoc, setUploadingDoc] = useState(null);
|
||
const [validatingDoc, setValidatingDoc] = useState(null);
|
||
const [docValidation, setDocValidation] = useState({
|
||
w9: null,
|
||
coi: null,
|
||
sos: null,
|
||
// contract: null, // Removed for contract upload status
|
||
});
|
||
const [showAIInsights, setShowAIInsights] = useState(false);
|
||
const [aiInsights, setAiInsights] = useState(null);
|
||
|
||
// Get invite data from URL
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const inviteCode = urlParams.get('invite');
|
||
|
||
const { data: invite } = useQuery({
|
||
queryKey: ['vendor-invite', inviteCode],
|
||
queryFn: async () => {
|
||
if (!inviteCode) return null;
|
||
const invites = await base44.entities.VendorInvite.filter({ invite_code: inviteCode });
|
||
return invites[0];
|
||
},
|
||
enabled: !!inviteCode
|
||
});
|
||
|
||
// Pre-fill data from invite
|
||
useEffect(() => {
|
||
if (invite) {
|
||
setFormData(prev => ({
|
||
...prev,
|
||
legal_name: invite.company_name || "",
|
||
primary_contact_name: invite.primary_contact_name || "",
|
||
primary_contact_email: invite.primary_contact_email || ""
|
||
}));
|
||
}
|
||
}, [invite]);
|
||
|
||
|
||
const steps = [
|
||
{ number: 0, title: "Welcome", icon: Sparkles, subtitle: "Let's get started" },
|
||
{ number: 1, title: "NDA & Trust", icon: Shield, subtitle: "Sign and accept agreement" },
|
||
{ number: 2, title: "Foodbuy Contract", icon: FileText, subtitle: "Review & acknowledge terms" },
|
||
{ number: 3, title: "Business Identity", icon: Building2, subtitle: "Let's set your stage" },
|
||
{ number: 4, title: "Documents & Validation", icon: Shield, subtitle: "Compliance essentials" },
|
||
{ number: 5, title: "Service Coverage", icon: MapPin, subtitle: "Where you operate" },
|
||
{ number: 6, title: "Rate Proposals", icon: DollarSign, subtitle: "Competitive pricing" },
|
||
{ number: 7, title: "AI Intelligence", icon: Sparkles, subtitle: "Market insights" }
|
||
];
|
||
|
||
const progressPercentage = currentStep === 0 ? 0 : ((currentStep - 1) / (steps.length - 2)) * 100;
|
||
|
||
// Upload document
|
||
const handleFileUpload = async (file, docType) => {
|
||
if (!file) return;
|
||
|
||
setUploadingDoc(docType);
|
||
try {
|
||
// Upload file
|
||
const { file_url } = await base44.integrations.Core.UploadFile({ file });
|
||
|
||
setFormData(prev => ({
|
||
...prev,
|
||
[`${docType}_url`]: file_url,
|
||
[`${docType}_file`]: file
|
||
}));
|
||
|
||
// For contract, the logic has been removed as it's handled by DocumentViewer
|
||
// and AI analysis of an uploaded contract is no longer directly here.
|
||
|
||
// Validate document with AI
|
||
setValidatingDoc(docType);
|
||
const validation = await validateDocument(file_url, docType);
|
||
|
||
setDocValidation(prev => ({
|
||
...prev,
|
||
[docType]: validation
|
||
}));
|
||
|
||
toast({
|
||
title: "Document Uploaded",
|
||
description: validation.isValid ? "✅ Document validated successfully" : "⚠️ Please review validation notes",
|
||
});
|
||
} catch (error) {
|
||
toast({
|
||
title: "Upload Failed",
|
||
description: error.message,
|
||
variant: "destructive"
|
||
});
|
||
} finally {
|
||
setUploadingDoc(null);
|
||
setValidatingDoc(null);
|
||
}
|
||
};
|
||
|
||
// Validate document with AI
|
||
const validateDocument = async (fileUrl, docType) => {
|
||
const prompts = {
|
||
w9: "Analyze this W-9 tax form and verify: 1) Business name is filled 2) Tax ID is present 3) Business entity type is checked 4) Signature is present. Return validation results.",
|
||
coi: "Analyze this Certificate of Insurance and verify: 1) General liability coverage is present 2) Coverage amount meets $1M minimum 3) Policy is not expired 4) KROW or client is listed as additional insured. Return validation results.",
|
||
sos: "Analyze this Secretary of State certificate and verify: 1) Business is actively registered 2) Registration is current/not expired 3) Business name matches application 4) State of registration. Return validation results."
|
||
};
|
||
|
||
try {
|
||
const analysis = await base44.integrations.Core.InvokeLLM({
|
||
prompt: prompts[docType],
|
||
file_urls: [fileUrl],
|
||
response_json_schema: {
|
||
type: "object",
|
||
properties: {
|
||
isValid: { type: "boolean" },
|
||
businessName: { type: "string" },
|
||
expiryDate: { type: "string" },
|
||
registrationState: { type: "string" },
|
||
issues: { type: "array", items: { type: "string" } },
|
||
notes: { type: "string" }
|
||
}
|
||
}
|
||
});
|
||
|
||
return analysis;
|
||
} catch (error) {
|
||
return {
|
||
isValid: false,
|
||
issues: ["Unable to validate document automatically"],
|
||
notes: "Manual review required"
|
||
};
|
||
}
|
||
};
|
||
|
||
// Generate AI rate suggestions - This function remains but its direct UI trigger in Step 4 is removed
|
||
const generateAIRates = async () => {
|
||
setGeneratingRates(true);
|
||
try {
|
||
const response = await base44.integrations.Core.InvokeLLM({
|
||
prompt: `
|
||
Generate competitive rate proposals for ${formData.legal_name}.
|
||
Business Type: ${formData.business_type}
|
||
Service Regions: ${Object.values(formData.selected_cities).flat().map(city => city).join(", ")}
|
||
Total Employees: ${formData.total_employees}
|
||
Software Type: ${formData.software_type}
|
||
|
||
Provide 10-15 common staffing roles with competitive rates for the hospitality/event industry.
|
||
Consider regional market rates and vendor's operational efficiency.
|
||
|
||
For each role, provide:
|
||
- Role name
|
||
- Category (Kitchen and Culinary, Concessions, Facilities, Bartending, Security, Event Staff, Management)
|
||
- Employee wage (competitive market rate)
|
||
- Suggested markup percentage (18-25% based on role complexity)
|
||
- Calculated client rate
|
||
`,
|
||
add_context_from_internet: true,
|
||
response_json_schema: {
|
||
type: "object",
|
||
properties: {
|
||
roles: {
|
||
type: "array",
|
||
items: {
|
||
type: "object",
|
||
properties: {
|
||
role_name: { type: "string" },
|
||
category: { type: "string" },
|
||
employee_wage: { type: "number" },
|
||
markup_percentage: { type: "number" },
|
||
client_rate: { type: "number" },
|
||
market_comparison: { type: "string" }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
if (response?.roles) {
|
||
setFormData(prev => ({
|
||
...prev,
|
||
rate_proposals: response.roles
|
||
}));
|
||
|
||
toast({
|
||
title: "Rates Generated",
|
||
description: `${response.roles.length} competitive rates suggested`,
|
||
});
|
||
}
|
||
} catch (error) {
|
||
toast({
|
||
title: "Generation Failed",
|
||
description: error.message,
|
||
variant: "destructive"
|
||
});
|
||
} finally {
|
||
setGeneratingRates(null);
|
||
}
|
||
};
|
||
|
||
// Generate AI insights
|
||
const generateAIInsights = async () => {
|
||
try {
|
||
const insights = await base44.integrations.Core.InvokeLLM({
|
||
prompt: `
|
||
Analyze this vendor application and provide strategic insights:
|
||
|
||
Vendor: ${formData.legal_name}
|
||
Business Type: ${formData.business_type}
|
||
Total Employees: ${formData.total_employees}
|
||
Software: ${formData.software_name || formData.software_type}
|
||
Service Areas: ${Object.values(formData.selected_cities).flat().map(city => city).join(", ")}
|
||
Rate Proposals: ${formData.rate_proposals.length} roles
|
||
|
||
Provide:
|
||
1. Competitive positioning (1-5 stars)
|
||
2. Market alignment assessment
|
||
3. Pricing competitiveness
|
||
4. Operational readiness score
|
||
5. Recommended focus areas
|
||
6. Potential challenges
|
||
`,
|
||
add_context_from_internet: true,
|
||
response_json_schema: {
|
||
type: "object",
|
||
properties: {
|
||
overall_score: { type: "number" },
|
||
competitive_position: { type: "string" },
|
||
market_alignment: { type: "string" },
|
||
pricing_analysis: { type: "string" },
|
||
operational_readiness: { type: "string" },
|
||
strengths: { type: "array", items: { type: "string" } },
|
||
recommendations: { type: "array", items: { type: "string" } },
|
||
potential_challenges: { type: "array", items: { type: "string" } }
|
||
}
|
||
}
|
||
});
|
||
|
||
setAiInsights(insights);
|
||
setShowAIInsights(true);
|
||
} catch (error) {
|
||
toast({
|
||
title: "Analysis Failed",
|
||
description: error.message,
|
||
variant: "destructive"
|
||
});
|
||
}
|
||
};
|
||
|
||
// US States and Major Cities Data
|
||
const usLocationData = {
|
||
"Alabama": ["Birmingham", "Montgomery", "Mobile", "Huntsville"],
|
||
"Alaska": ["Anchorage", "Fairbanks", "Juneau"],
|
||
"Arizona": ["Phoenix", "Tucson", "Mesa", "Scottsdale", "Chandler"],
|
||
"Arkansas": ["Little Rock", "Fort Smith", "Fayetteville"],
|
||
"California": ["Los Angeles", "San Francisco", "San Diego", "San Jose", "Sacramento", "Oakland", "Fresno", "Long Beach", "Anaheim", "Bakersfield"],
|
||
"Colorado": ["Denver", "Colorado Springs", "Aurora", "Boulder", "Fort Collins"],
|
||
"Connecticut": ["Hartford", "New Haven", "Stamford", "Bridgeport"],
|
||
"Delaware": ["Wilmington", "Dover", "Newark"],
|
||
"Florida": ["Miami", "Orlando", "Tampa", "Jacksonville", "Fort Lauderdale", "West Palm Beach"],
|
||
"Georgia": ["Atlanta", "Savannah", "Augusta", "Columbus", "Macon"],
|
||
"Hawaii": ["Honolulu", "Hilo", "Kailua"],
|
||
"Idaho": ["Boise", "Meridian", "Nampa"],
|
||
"Illinois": ["Chicago", "Aurora", "Naperville", "Rockford", "Joliet"],
|
||
"Indiana": ["Indianapolis", "Fort Wayne", "Evansville", "South Bend"],
|
||
"Iowa": ["Des Moines", "Cedar Rapids", "Davenport"],
|
||
"Kansas": ["Wichita", "Overland Park", "Kansas City", "Topeka"],
|
||
"Kentucky": ["Louisville", "Lexington", "Bowling Green"],
|
||
"Louisiana": ["New Orleans", "Baton Rouge", "Shreveport", "Lafayette"],
|
||
"Maine": ["Portland", "Lewiston", "Bangor"],
|
||
"Maryland": ["Baltimore", "Columbia", "Germantown", "Silver Spring"],
|
||
"Massachusetts": ["Boston", "Worcester", "Springfield", "Cambridge", "Lowell"],
|
||
"Michigan": ["Detroit", "Grand Rapids", "Warren", "Sterling Heights", "Ann Arbor"],
|
||
"Minnesota": ["Minneapolis", "St. Paul", "Rochester", "Duluth"],
|
||
"Mississippi": ["Jackson", "Gulfport", "Southaven"],
|
||
"Missouri": ["Kansas City", "St. Louis", "Springfield", "Columbia"],
|
||
"Montana": ["Billings", "Missoula", "Great Falls"],
|
||
"Nebraska": ["Omaha", "Lincoln", "Bellevue"],
|
||
"Nevada": ["Las Vegas", "Henderson", "Reno", "North Las Vegas"],
|
||
"New Hampshire": ["Manchester", "Nashua", "Concord"],
|
||
"New Jersey": ["Newark", "Jersey City", "Paterson", "Elizabeth", "Edison"],
|
||
"New Mexico": ["Albuquerque", "Las Cruces", "Rio Rancho", "Santa Fe"],
|
||
"New York": ["New York City", "Buffalo", "Rochester", "Yonkers", "Syracuse", "Albany"],
|
||
"North Carolina": ["Charlotte", "Raleigh", "Greensboro", "Durham", "Winston-Salem"],
|
||
"North Dakota": ["Fargo", "Bismarck", "Grand Forks"],
|
||
"Ohio": ["Columbus", "Cleveland", "Cincinnati", "Toledo", "Akron"],
|
||
"Oklahoma": ["Oklahoma City", "Tulsa", "Norman"],
|
||
"Oregon": ["Portland", "Salem", "Eugene", "Gresham"],
|
||
"Pennsylvania": ["Philadelphia", "Pittsburgh", "Allentown", "Erie", "Reading"],
|
||
"Rhode Island": ["Providence", "Warwick", "Cranston"],
|
||
"South Carolina": ["Charleston", "Columbia", "North Charleston", "Mount Pleasant"],
|
||
"South Dakota": ["Sioux Falls", "Rapid City", "Aberdeen"],
|
||
"Tennessee": ["Nashville", "Memphis", "Knoxville", "Chattanooga"],
|
||
"Texas": ["Houston", "San Antonio", "Dallas", "Austin", "Fort Worth", "El Paso", "Arlington", "Corpus Christi"],
|
||
"Utah": ["Salt Lake City", "West Valley City", "Provo", "West Jordan"],
|
||
"Vermont": ["Burlington", "South Burlington", "Rutland"],
|
||
"Virginia": ["Virginia Beach", "Norfolk", "Chesapeake", "Richmond", "Newport News"],
|
||
"Washington": ["Seattle", "Spokane", "Tacoma", "Vancouver", "Bellevue"],
|
||
"West Virginia": ["Charleston", "Huntington", "Morgantown"],
|
||
"Wisconsin": ["Milwaukee", "Madison", "Green Bay", "Kenosha"],
|
||
"Wyoming": ["Cheyenne", "Casper", "Laramie"]
|
||
};
|
||
|
||
// Submit vendor application - UPDATED
|
||
const submitApplicationMutation = useMutation({
|
||
mutationFn: async () => {
|
||
// Generate vendor number
|
||
const vendorNumber = `VN-${Math.floor(1000 + Math.random() * 9000)}`;
|
||
|
||
// Build coverage regions from selected cities
|
||
const coverageRegionsList = [];
|
||
Object.keys(formData.selected_cities).forEach(state => {
|
||
formData.selected_cities[state].forEach(city => {
|
||
coverageRegionsList.push(`${city}, ${state}`);
|
||
});
|
||
});
|
||
|
||
// Create vendor record
|
||
const vendor = await base44.entities.Vendor.create({
|
||
vendor_number: vendorNumber,
|
||
legal_name: formData.legal_name,
|
||
doing_business_as: formData.dba,
|
||
tax_id: formData.tax_id,
|
||
business_type: formData.business_type,
|
||
primary_contact_name: formData.primary_contact_name,
|
||
primary_contact_email: formData.primary_contact_email,
|
||
primary_contact_phone: formData.primary_contact_phone,
|
||
// Updated logic for billing_address and service_address based on new flow
|
||
service_address: formData.service_address,
|
||
billing_address: formData.same_as_billing ? formData.service_address : formData.billing_address,
|
||
coverage_regions: coverageRegionsList,
|
||
w9_document: formData.w9_url,
|
||
coi_document: formData.coi_url,
|
||
insurance_certificate: formData.coi_url,
|
||
approval_status: "pending",
|
||
is_active: false,
|
||
// Updated contract note to reflect review not upload
|
||
notes: `Total Employees: ${formData.total_employees}, Software: ${formData.software_name || formData.software_type}, SOS: ${formData.sos_url ? 'Verified' : 'Pending'}, Contract: ${formData.contract_acknowledged ? 'Acknowledged' : 'Not Acknowledged'}. Contract Review Notes: ${formData.contract_review_notes}. NDA Signed: ${formData.nda_acknowledged ? 'Yes' : 'No'}`
|
||
});
|
||
|
||
// Create rate proposals with location-specific rates
|
||
const vendorFee = invite?.vendor_admin_fee || 12;
|
||
|
||
await Promise.all(
|
||
formData.rate_proposals
|
||
.filter(rate => rate.is_active)
|
||
.map(rate =>
|
||
base44.entities.VendorRate.create({
|
||
vendor_id: vendor.id,
|
||
vendor_name: formData.legal_name,
|
||
category: rate.category,
|
||
role_name: rate.role_name,
|
||
employee_wage: rate.employee_wage,
|
||
markup_percentage: rate.markup_percentage,
|
||
vendor_fee_percentage: vendorFee,
|
||
client_rate: rate.client_rate,
|
||
is_active: true,
|
||
ai_analysis: rate.ai_analysis,
|
||
is_custom: rate.is_custom,
|
||
notes: rate.location_rates ? JSON.stringify(rate.location_rates) : null
|
||
})
|
||
)
|
||
);
|
||
|
||
// Update invite status if exists
|
||
if (invite) {
|
||
await base44.entities.VendorInvite.update(invite.id, {
|
||
invite_status: "completed",
|
||
vendor_id: vendor.id,
|
||
invite_accepted_date: new Date().toISOString()
|
||
});
|
||
}
|
||
|
||
// Send confirmation email
|
||
await base44.integrations.Core.SendEmail({
|
||
from_name: "KROW Platform",
|
||
to: formData.primary_contact_email,
|
||
subject: "Vendor Application Received - Under Review",
|
||
body: `
|
||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
|
||
<h2>Application Received Successfully!</h2>
|
||
<p>Dear ${formData.primary_contact_name},</p>
|
||
<p>Thank you for completing your vendor application with KROW.</p>
|
||
<div style="background: #f0f9ff; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||
<p style="margin: 0;"><strong>Application Summary:</strong></p>
|
||
<p style="margin: 8px 0;">Vendor Number: <strong>${vendorNumber}</strong></p>
|
||
<p style="margin: 8px 0;">Company: <strong>${formData.legal_name}</strong></p>
|
||
<p style="margin: 8px 0;">Status: <strong>Under Review</strong></p>
|
||
</div>
|
||
<p>Our procurement team will review your application and get back to you within 2-3 business days.</p>
|
||
<p>Best regards,<br>KROW Team</p>
|
||
</div>
|
||
`
|
||
});
|
||
|
||
return vendor;
|
||
},
|
||
onSuccess: (vendor) => {
|
||
toast({
|
||
title: "Application Submitted!",
|
||
description: "You'll hear from us within 2-3 business days",
|
||
});
|
||
navigate(createPageUrl("VendorManagement"));
|
||
},
|
||
onError: (error) => {
|
||
toast({
|
||
title: "Submission Failed",
|
||
description: error.message,
|
||
variant: "destructive"
|
||
});
|
||
}
|
||
});
|
||
|
||
const handleNext = () => {
|
||
// Step 0: Welcome (no validation, just go to next step)
|
||
if (currentStep === 0) {
|
||
setCurrentStep(1);
|
||
return;
|
||
}
|
||
|
||
// Step 1: NDA
|
||
if (currentStep === 1) {
|
||
if (!formData.nda_acknowledged) {
|
||
toast({
|
||
title: "NDA Required",
|
||
description: "You must sign and accept the NDA before proceeding",
|
||
variant: "destructive"
|
||
});
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Step 2: Foodbuy Contract
|
||
if (currentStep === 2) {
|
||
if (!formData.contract_acknowledged || !formData.contract_va_fee_acknowledged) {
|
||
toast({
|
||
title: "Contract Review Required",
|
||
description: "Please acknowledge all terms to proceed",
|
||
variant: "destructive"
|
||
});
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Step 3: Business Identity
|
||
if (currentStep === 3) {
|
||
if (!formData.legal_name || !formData.tax_id || !formData.business_type ||
|
||
!formData.primary_contact_name || !formData.primary_contact_email ||
|
||
!formData.service_address || !formData.total_employees ||
|
||
(formData.has_software === "yes" && (!formData.software_type || !formData.software_name))) {
|
||
toast({
|
||
title: "Missing Information",
|
||
description: "Please fill in all required fields",
|
||
variant: "destructive"
|
||
});
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Step 4: Documents
|
||
if (currentStep === 4) {
|
||
const isSosValidated = (docValidation.sos?.autoVerified && docValidation.sos.isRegistered && (docValidation.sos.registrationStatus === 'Active' || docValidation.sos.registrationStatus === 'Good Standing')) || (docValidation.sos && docValidation.sos.isValid);
|
||
|
||
if (!formData.w9_url || !formData.coi_url || !isSosValidated) {
|
||
toast({
|
||
title: "Missing Documents",
|
||
description: "Please upload W-9, COI, and verify/upload Secretary of State certificate",
|
||
variant: "destructive"
|
||
});
|
||
return;
|
||
}
|
||
|
||
// NEW: Validate compliance attestations
|
||
if (!formData.background_check_attestation || !formData.i9_compliance_attestation || !formData.legal_compliance_attestation) {
|
||
toast({
|
||
title: "Compliance Attestation Required",
|
||
description: "Please confirm all compliance attestations to proceed",
|
||
variant: "destructive"
|
||
});
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Step 5: Service Coverage
|
||
if (currentStep === 5) {
|
||
const totalCities = Object.values(formData.selected_cities).reduce((sum, cities) => sum + cities.length, 0);
|
||
if (totalCities === 0) {
|
||
toast({
|
||
title: "No Coverage Selected",
|
||
description: "Please select at least one city/location you can service",
|
||
variant: "destructive"
|
||
});
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Step 6: Rate Proposals
|
||
if (currentStep === 6) {
|
||
const activeRates = formData.rate_proposals.filter(rate => rate.is_active);
|
||
if (activeRates.length === 0) {
|
||
toast({
|
||
title: "No Active Rate Proposals",
|
||
description: "Please activate at least one rate proposal.",
|
||
variant: "destructive"
|
||
});
|
||
return;
|
||
}
|
||
|
||
const hasInvalidRates = activeRates.some(rate =>
|
||
!rate.role_name || !rate.category || rate.employee_wage <= 0 || rate.markup_percentage <= 0 || rate.client_rate <= 0
|
||
);
|
||
if (hasInvalidRates) {
|
||
toast({
|
||
title: "Incomplete Rate Proposals",
|
||
description: "Please ensure all active rate proposals have valid employee wages and markups.",
|
||
variant: "destructive"
|
||
});
|
||
return;
|
||
}
|
||
}
|
||
|
||
|
||
if (currentStep < steps.length - 1) {
|
||
setCurrentStep(prev => prev + 1);
|
||
}
|
||
};
|
||
|
||
const handleBack = () => {
|
||
if (currentStep > 0) {
|
||
setCurrentStep(prev => prev - 1);
|
||
}
|
||
};
|
||
|
||
const handleSubmit = () => {
|
||
submitApplicationMutation.mutate();
|
||
};
|
||
|
||
// Update handleSignNDA to handle signature data from DocumentViewer
|
||
const handleSignNDA = (acknowledgmentData) => {
|
||
const now = new Date();
|
||
setFormData(prev => ({
|
||
...prev,
|
||
nda_acknowledged: true,
|
||
nda_signed_by: acknowledgmentData.signerName || invite?.primary_contact_name || prev.primary_contact_name || "Vendor",
|
||
nda_signature_date: now.toLocaleDateString(),
|
||
nda_signature_time: now.toLocaleTimeString(),
|
||
nda_signature_image: acknowledgmentData.signature || "" // Store the signature image
|
||
}));
|
||
toast({
|
||
title: "✅ NDA Signed",
|
||
description: `Signed on ${now.toLocaleString()}`,
|
||
});
|
||
};
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-purple-50 py-8 px-4">
|
||
<div className="max-w-5xl mx-auto">
|
||
{/* STEP 0: Welcome Screen */}
|
||
{currentStep === 0 && (
|
||
<Card className="border-2 border-[#0A39DF]/20 shadow-2xl">
|
||
<CardContent className="p-12">
|
||
<div className="text-center space-y-6">
|
||
{/* Logo - KROW LOGO */}
|
||
<div className="flex justify-center mb-6">
|
||
<div className="w-32 h-32 bg-gradient-to-r from-[#0A39DF] to-[#1C323E] rounded-full flex items-center justify-center shadow-xl p-6">
|
||
<img
|
||
src="https://qtrypzzcjebvfcihiynt.supabase.co/storage/v1/object/public/base44-prod/public/68fc6cf01386035c266e7a5d/3ba390829_KROWlogo.png"
|
||
alt="KROW Logo"
|
||
className="w-full h-full object-contain"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Welcome Message */}
|
||
<div>
|
||
<h1 className="text-4xl font-bold bg-gradient-to-r from-[#0A39DF] to-[#1C323E] bg-clip-text text-transparent mb-4">
|
||
Welcome to KROW
|
||
</h1>
|
||
<p className="text-xl text-slate-600 leading-relaxed max-w-2xl mx-auto">
|
||
The Workforce Control Tower that connects you to the most trusted procurement and operator network.
|
||
</p>
|
||
<p className="text-lg text-slate-500 mt-4">
|
||
Let's begin your onboarding journey, it'll take just a few minutes to get your company fully connected.
|
||
</p>
|
||
</div>
|
||
|
||
{/* Progress indicator */}
|
||
<div className="pt-8">
|
||
<div className="flex items-center justify-center gap-2 mb-4">
|
||
<div className="w-full max-w-md bg-slate-200 h-3 rounded-full overflow-hidden">
|
||
<div
|
||
className="h-full bg-gradient-to-r from-[#0A39DF] to-[#1C323E] transition-all duration-500"
|
||
style={{ width: '0%' }}
|
||
/>
|
||
</div>
|
||
<span className="text-sm font-semibold text-slate-600 whitespace-nowrap">0% Complete</span>
|
||
</div>
|
||
<p className="text-sm text-slate-500">Step 1 of 8: NDA & Trust Agreement</p>
|
||
</div>
|
||
|
||
{/* Start Button */}
|
||
<Button
|
||
onClick={handleNext}
|
||
className="mt-8 bg-gradient-to-r from-[#0A39DF] to-[#1C323E] hover:from-[#0A39DF]/90 hover:to-[#1C323E]/90 text-white px-12 py-6 text-lg font-bold shadow-xl"
|
||
>
|
||
Start Onboarding
|
||
<ArrowRight className="w-5 h-5 ml-2" />
|
||
</Button>
|
||
|
||
{invite && (
|
||
<Badge className="mt-6 bg-green-100 text-green-700 px-4 py-2">
|
||
✅ Invited by {invite.invited_by}
|
||
</Badge>
|
||
)}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
|
||
{/* Steps 1-7: Show header and progress only if not on welcome screen */}
|
||
{currentStep > 0 && (
|
||
<>
|
||
{/* Header */}
|
||
<div className="text-center mb-8">
|
||
<div className="flex items-center justify-center gap-2 mb-4">
|
||
<img
|
||
src="https://qtrypzzcjebvfcihiynt.supabase.co/storage/v1/object/public/base44-prod/public/68fc6cf01386035c266e7a5d/3ba390829_KROWlogo.png"
|
||
alt="KROW Logo"
|
||
className="w-8 h-8"
|
||
/>
|
||
<h1 className="text-4xl font-bold bg-gradient-to-r from-[#0A39DF] to-[#1C323E] bg-clip-text text-transparent">
|
||
Smart Vendor Onboarding
|
||
</h1>
|
||
</div>
|
||
<p className="text-slate-600">Real-time market analysis • Predictive scoring • Instant validation</p>
|
||
{invite && (
|
||
<Badge className="mt-2 bg-green-100 text-green-700">
|
||
✅ Invited by {invite.invited_by}
|
||
</Badge>
|
||
)}
|
||
</div>
|
||
|
||
{/* Progress Bar */}
|
||
<Card className="mb-6 border-2 border-[#0A39DF]/20">
|
||
<CardContent className="p-6">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<p className="text-sm font-semibold text-slate-700">Step {currentStep} of {steps.length - 1}</p>
|
||
<p className="text-sm font-semibold text-[#0A39DF]">{Math.round(progressPercentage)}% Complete</p>
|
||
</div>
|
||
<Progress value={progressPercentage} className="h-3 mb-6" />
|
||
|
||
{/* Step indicators */}
|
||
<div className="grid grid-cols-7 gap-2">
|
||
{steps.slice(1).map((step) => {
|
||
const Icon = step.icon;
|
||
const isActive = currentStep === step.number;
|
||
const isComplete = currentStep > step.number;
|
||
|
||
return (
|
||
<button
|
||
key={step.number}
|
||
onClick={() => setCurrentStep(step.number)}
|
||
className={`p-3 rounded-lg text-center transition-all ${
|
||
isActive
|
||
? 'bg-[#0A39DF] text-white shadow-lg scale-105'
|
||
: isComplete
|
||
? 'bg-green-100 text-green-700'
|
||
: 'bg-slate-100 text-slate-400'
|
||
}`}
|
||
>
|
||
<Icon className="w-5 h-5 mx-auto mb-1" />
|
||
<p className="text-xs font-medium truncate">{step.title}</p>
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</>
|
||
)}
|
||
|
||
{/* STEP 1: NDA & Trust Agreement */}
|
||
{currentStep === 1 && (
|
||
<Card className="border-2 border-slate-200">
|
||
<CardHeader className="bg-gradient-to-r from-purple-50 to-indigo-50 border-b">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-12 h-12 bg-gradient-to-r from-purple-600 to-indigo-600 rounded-xl flex items-center justify-center">
|
||
<Shield className="w-6 h-6 text-white" />
|
||
</div>
|
||
<div>
|
||
<CardTitle className="text-2xl">NDA & Trust Agreement</CardTitle>
|
||
<p className="text-sm text-slate-600 mt-1">Sign and accept - Date and time will be recorded</p>
|
||
</div>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent className="p-6 space-y-6">
|
||
{/* NDA Document Viewer */}
|
||
<DocumentViewer
|
||
documentUrl="https://qtrypzzcjebvfcihiynt.supabase.co/storage/v1/object/public/base44-prod/public/68fc6cf01386035c266e7a5d/187424574_LegendaryEventStaffingFOODBUYVendorNDA.pdf"
|
||
documentName="Confidentiality & Non-Disclosure Agreement"
|
||
documentType="NDA"
|
||
onAcknowledge={handleSignNDA}
|
||
initialNotes=""
|
||
isAcknowledged={formData.nda_acknowledged}
|
||
timeSpent={0}
|
||
/>
|
||
|
||
{/* Warning if not signed */}
|
||
{!formData.nda_acknowledged && (
|
||
<div className="p-4 bg-purple-50 border border-purple-200 rounded-lg flex items-start gap-2">
|
||
<AlertCircle className="w-5 h-5 text-purple-600 mt-0.5 flex-shrink-0" />
|
||
<p className="text-sm text-purple-800">
|
||
You must review and sign the NDA to proceed with onboarding
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* Success message if signed - NOW WITH SIGNATURE IMAGE */}
|
||
{formData.nda_acknowledged && (
|
||
<div className="p-6 bg-green-50 border-2 border-green-300 rounded-xl">
|
||
<div className="text-center">
|
||
<div className="w-16 h-16 bg-green-500 rounded-full flex items-center justify-center mx-auto mb-4">
|
||
<Check className="w-10 h-10 text-white" />
|
||
</div>
|
||
<h4 className="text-xl font-bold text-green-900 mb-3">✅ NDA Signed Successfully</h4>
|
||
|
||
{/* Signature and Details Card */}
|
||
<div className="bg-white p-6 rounded-lg border-2 border-green-300 max-w-md mx-auto">
|
||
{/* Signer Name */}
|
||
<div className="mb-4">
|
||
<p className="text-xs text-slate-500 uppercase tracking-wide mb-1">Signed by:</p>
|
||
<p className="text-lg font-bold text-slate-900">{formData.nda_signed_by}</p>
|
||
</div>
|
||
|
||
{/* Signature Image */}
|
||
{formData.nda_signature_image && (
|
||
<div className="mb-4 p-4 bg-slate-50 rounded-lg border-2 border-slate-300">
|
||
<p className="text-xs text-slate-500 uppercase tracking-wide mb-2">Digital Signature:</p>
|
||
<img
|
||
src={formData.nda_signature_image}
|
||
alt="Signature"
|
||
className="w-full h-24 object-contain bg-white rounded border border-slate-300"
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
{/* Date and Time */}
|
||
<div className="space-y-2 text-left text-sm border-t border-slate-200 pt-4">
|
||
<div className="flex justify-between">
|
||
<span className="font-semibold text-slate-700">Date:</span>
|
||
<span className="text-slate-900">{formData.nda_signature_date}</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="font-semibold text-slate-700">Time:</span>
|
||
<span className="text-slate-900">{formData.nda_signature_time}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<p className="text-green-700 mt-4 font-semibold">
|
||
✓ You may now proceed to the next step
|
||
</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
|
||
{/* STEP 2: Foodbuy Contract Review (was Step 1) */}
|
||
{currentStep === 2 && (
|
||
<Card className="border-2 border-slate-200">
|
||
<CardHeader className="bg-gradient-to-r from-blue-50 to-indigo-50 border-b">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-12 h-12 bg-gradient-to-r from-blue-600 to-indigo-600 rounded-xl flex items-center justify-center">
|
||
<FileText className="w-6 h-6 text-white" />
|
||
</div>
|
||
<div>
|
||
<CardTitle className="text-2xl">Foodbuy Contract Review</CardTitle>
|
||
<p className="text-sm text-slate-600 mt-1">Please review the terms before proceeding</p>
|
||
</div>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent className="p-6 space-y-6">
|
||
{/* Contract Document Viewer */}
|
||
<DocumentViewer
|
||
documentUrl="https://qtrypzzcjebvfcihiynt.supabase.co/storage/v1/object/public/base44-prod/public/68fc6cf01386035c266e7a5d/2c22905a2_FoodbuyDraftContract.pdf"
|
||
documentName="Foodbuy Temporary Staffing Agreement"
|
||
documentType="Contract"
|
||
onAcknowledge={(acknowledgmentData) => {
|
||
setFormData(prev => ({
|
||
...prev,
|
||
contract_review_notes: acknowledgmentData.notes || prev.contract_review_notes,
|
||
contract_review_time: acknowledgmentData.reviewTime || prev.contract_review_time,
|
||
contract_acknowledged: true
|
||
}));
|
||
}}
|
||
initialNotes={formData.contract_review_notes}
|
||
isAcknowledged={formData.contract_acknowledged}
|
||
timeSpent={formData.contract_review_time}
|
||
/>
|
||
|
||
{/* Performance Requirements & VA Fee Structure Breakdown */}
|
||
<div className="mt-6 space-y-4">
|
||
<div className="bg-gradient-to-br from-blue-50 to-indigo-50 p-6 rounded-xl border-2 border-blue-200">
|
||
<h3 className="text-lg font-bold text-[#1C323E] mb-4 flex items-center gap-2">
|
||
<Target className="w-6 h-6 text-blue-600" />
|
||
Performance Requirements & Service Standards
|
||
</h3>
|
||
|
||
{/* Fill Rate & KPIs */}
|
||
<div className="bg-white rounded-lg p-5 mb-4 shadow-sm border border-blue-100">
|
||
<h4 className="font-semibold text-[#1C323E] mb-3 flex items-center gap-2">
|
||
<TrendingUp className="w-5 h-5 text-green-600" />
|
||
Key Performance Indicators (KPIs)
|
||
</h4>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<div className="flex justify-between items-center p-3 bg-slate-50 rounded-lg">
|
||
<span className="text-sm font-medium text-slate-700">Fill Rate Target</span>
|
||
<Badge className="bg-green-100 text-green-700 font-bold">≥ 95%</Badge>
|
||
</div>
|
||
<div className="flex justify-between items-center p-3 bg-slate-50 rounded-lg">
|
||
<span className="text-sm font-medium text-slate-700">On-Time Arrival</span>
|
||
<Badge className="bg-green-100 text-green-700 font-bold">≥ 98%</Badge>
|
||
</div>
|
||
<div className="flex justify-between items-center p-3 bg-slate-50 rounded-lg">
|
||
<span className="text-sm font-medium text-slate-700">Client Satisfaction (CSAT)</span>
|
||
<Badge className="bg-green-100 text-green-700 font-bold">≥ 4.5/5.0</Badge>
|
||
</div>
|
||
<div className="flex justify-between items-center p-3 bg-slate-50 rounded-lg">
|
||
<span className="text-sm font-medium text-slate-700">Cancellation Rate</span>
|
||
<Badge className="bg-amber-100 text-amber-700 font-bold">≤ 3%</Badge>
|
||
</div>
|
||
<div className="flex justify-between items-center p-3 bg-slate-50 rounded-lg">
|
||
<span className="text-sm font-medium text-slate-700">No-Show Rate</span>
|
||
<Badge className="bg-amber-100 text-amber-700 font-bold">≤ 1%</Badge>
|
||
</div>
|
||
<div className="flex justify-between items-center p-3 bg-slate-50 rounded-lg">
|
||
<span className="text-sm font-medium text-slate-700">Response Time</span>
|
||
<Badge className="bg-blue-100 text-blue-700 font-bold">≤ 2 hours</Badge>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Compliance & Attestations */}
|
||
<div className="bg-white rounded-lg p-5 mb-4 shadow-sm border border-blue-100">
|
||
<h4 className="font-semibold text-[#1C323E] mb-3 flex items-center gap-2">
|
||
<Shield className="w-5 h-5 text-purple-600" />
|
||
Compliance & Attestation Requirements
|
||
</h4>
|
||
<div className="space-y-3">
|
||
<div className="flex items-start gap-3 p-3 bg-green-50 rounded-lg border border-green-200">
|
||
<CheckCircle2 className="w-5 h-5 text-green-600 mt-0.5 flex-shrink-0" />
|
||
<div>
|
||
<p className="text-sm font-semibold text-green-900">Background Check Compliance</p>
|
||
<p className="text-xs text-green-700 mt-1">All employees must have valid background checks per federal and state requirements</p>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-start gap-3 p-3 bg-green-50 rounded-lg border border-green-200">
|
||
<CheckCircle2 className="w-5 h-5 text-green-600 mt-0.5 flex-shrink-0" />
|
||
<div>
|
||
<p className="text-sm font-semibold text-green-900">I-9 Employment Eligibility</p>
|
||
<p className="text-xs text-green-700 mt-1">Maintain complete and valid I-9 forms for all employees with proper documentation</p>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-start gap-3 p-3 bg-green-50 rounded-lg border border-green-200">
|
||
<CheckCircle2 className="w-5 h-5 text-green-600 mt-0.5 flex-shrink-0" />
|
||
<div>
|
||
<p className="text-sm font-semibold text-green-900">Insurance & Liability Coverage</p>
|
||
<p className="text-xs text-green-700 mt-1">General liability insurance minimum $1M, Workers' Compensation as required by law</p>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-start gap-3 p-3 bg-green-50 rounded-lg border border-green-200">
|
||
<CheckCircle2 className="w-5 h-5 text-green-600 mt-0.5 flex-shrink-0" />
|
||
<div>
|
||
<p className="text-sm font-semibold text-green-900">Legal & Regulatory Compliance</p>
|
||
<p className="text-xs text-green-700 mt-1">Comply with all federal, state, and local employment laws including wage & hour, safety, and anti-discrimination</p>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-start gap-3 p-3 bg-green-50 rounded-lg border border-green-200">
|
||
<CheckCircle2 className="w-5 h-5 text-green-600 mt-0.5 flex-shrink-0" />
|
||
<div>
|
||
<p className="text-sm font-semibold text-green-900">Tax & Payroll Compliance</p>
|
||
<p className="text-xs text-green-700 mt-1">Proper W-9 on file, accurate tax withholding, and timely payment of all employment taxes</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* VA Fee Structure */}
|
||
<div className="bg-white rounded-lg p-5 shadow-sm border border-blue-100">
|
||
<h4 className="font-semibold text-[#1C323E] mb-3 flex items-center gap-2">
|
||
<DollarSign className="w-5 h-5 text-purple-600" />
|
||
Vendor Fee Structure
|
||
</h4>
|
||
<div className="space-y-3">
|
||
<div className="p-4 bg-purple-50 rounded-lg border-2 border-purple-200">
|
||
<div className="flex items-center justify-between mb-2">
|
||
<span className="text-sm font-medium text-purple-900">Vendor Fee</span>
|
||
<Badge className="bg-purple-600 text-white text-lg font-bold px-4 py-1">
|
||
{invite?.vendor_admin_fee || 12}%
|
||
</Badge>
|
||
</div>
|
||
<p className="text-xs text-purple-700">
|
||
Cost of doing business with Foodbuy as your partner
|
||
</p>
|
||
</div>
|
||
|
||
<div className="p-4 bg-slate-50 rounded-lg border border-slate-200">
|
||
<h5 className="text-sm font-semibold text-[#1C323E] mb-3">Fee Calculation Example</h5>
|
||
<div className="space-y-2 text-sm">
|
||
<div className="flex justify-between">
|
||
<span className="text-slate-600">Employee Wage</span>
|
||
<span className="font-semibold">$18.50/hr</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-slate-600">Your Markup (20%)</span>
|
||
<span className="font-semibold text-blue-600">+$3.70/hr</span>
|
||
</div>
|
||
<div className="flex justify-between pt-2 border-t border-slate-300">
|
||
<span className="text-slate-700 font-medium">Bill Rate (to client)</span>
|
||
<span className="font-semibold">$22.20/hr</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-slate-600">Vendor Fee ({invite?.vendor_admin_fee || 12}%)</span>
|
||
<span className="font-semibold text-red-600">-${((22.20 * (invite?.vendor_admin_fee || 12)) / 100).toFixed(2)}/hr</span>
|
||
</div>
|
||
<div className="flex justify-between pt-2 border-t-2 border-slate-400">
|
||
<span className="text-[#1C323E] font-bold">You Receive</span>
|
||
<span className="font-bold text-green-600 text-lg">${(22.20 - (22.20 * (invite?.vendor_admin_fee || 12) / 100)).toFixed(2)}/hr</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="p-3 bg-blue-50 rounded-lg border border-blue-200">
|
||
<p className="text-xs text-blue-900">
|
||
<strong>💡 What the Vendor Fee Covers:</strong> Platform access, procurement management, client relationships, compliance oversight, payment processing, insurance coordination, and KROW system support.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Acknowledgment Checkbox */}
|
||
<div className="bg-slate-50 p-6 rounded-lg border-2 border-slate-200">
|
||
<div className="flex items-start gap-3">
|
||
<Checkbox
|
||
id="contract_va_fee_acknowledged"
|
||
checked={formData.contract_va_fee_acknowledged}
|
||
onCheckedChange={(checked) => setFormData(prev => ({ ...prev, contract_va_fee_acknowledged: checked }))}
|
||
className="mt-1 data-[state=checked]:bg-[#0A39DF] data-[state=checked]:border-[#0A39DF]"
|
||
/>
|
||
<div className="flex-1">
|
||
<Label htmlFor="contract_va_fee_acknowledged" className="font-semibold cursor-pointer text-base">
|
||
I understand the performance requirements and vendor fee structure <span className="text-red-500">*</span>
|
||
</Label>
|
||
<p className="text-sm text-slate-600 mt-1">
|
||
Including the {invite?.vendor_admin_fee || 12}% vendor fee and service standards outlined above
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{(!formData.contract_acknowledged || !formData.contract_va_fee_acknowledged) && (
|
||
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg flex items-start gap-2">
|
||
<AlertCircle className="w-5 h-5 text-blue-600 mt-0.5 flex-shrink-0" />
|
||
<p className="text-sm text-blue-800">
|
||
You must review and acknowledge the contract terms to proceed with onboarding
|
||
</p>
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
|
||
{/* STEP 3: Business Identity (was Step 2) */}
|
||
{currentStep === 3 && (
|
||
<Card className="border-2 border-slate-200">
|
||
<CardHeader className="bg-gradient-to-r from-slate-50 to-blue-50 border-b">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-12 h-12 bg-[#0A39DF] rounded-xl flex items-center justify-center">
|
||
<Building2 className="w-6 h-6 text-white" />
|
||
</div>
|
||
<div>
|
||
<CardTitle className="text-2xl">Business Identity</CardTitle>
|
||
<p className="text-sm text-slate-600 mt-1">Let's set your stage</p>
|
||
</div>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent className="p-6 space-y-6">
|
||
{/* Legal Business Name */}
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<Label htmlFor="legal_name">
|
||
Legal Business Name <span className="text-red-500">*</span>
|
||
</Label>
|
||
<Input
|
||
id="legal_name"
|
||
value={formData.legal_name}
|
||
onChange={(e) => setFormData(prev => ({ ...prev, legal_name: e.target.value }))}
|
||
placeholder="ABC Staffing Solutions LLC"
|
||
className="mt-2"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<Label htmlFor="dba">Doing Business As (DBA)</Label>
|
||
<Input
|
||
id="dba"
|
||
value={formData.dba}
|
||
onChange={(e) => setFormData(prev => ({ ...prev, dba: e.target.value }))}
|
||
placeholder="ABC Staffing"
|
||
className="mt-2"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Tax ID & Business Type */}
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<Label htmlFor="tax_id">
|
||
Federal Tax ID / EIN <span className="text-red-500">*</span>
|
||
</Label>
|
||
<Input
|
||
id="tax_id"
|
||
value={formData.tax_id}
|
||
onChange={(e) => setFormData(prev => ({ ...prev, tax_id: e.target.value }))}
|
||
placeholder="XX-XXXXXXX"
|
||
className="mt-2"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<Label htmlFor="business_type">
|
||
Business Entity <span className="text-red-500">*</span>
|
||
</Label>
|
||
<Select value={formData.business_type} onValueChange={(value) => setFormData(prev => ({ ...prev, business_type: value }))}>
|
||
<SelectTrigger className="mt-2">
|
||
<SelectValue placeholder="Select entity type" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="LLC">LLC</SelectItem>
|
||
<SelectItem value="Corporation">Corporation</SelectItem>
|
||
<SelectItem value="Partnership">Partnership</SelectItem>
|
||
<SelectItem value="Sole Proprietorship">Sole Proprietorship</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Contact Information */}
|
||
<div className="grid grid-cols-3 gap-4">
|
||
<div>
|
||
<Label htmlFor="contact_name">
|
||
Primary Contact Name <span className="text-red-500">*</span>
|
||
</Label>
|
||
<Input
|
||
id="contact_name"
|
||
value={formData.primary_contact_name}
|
||
onChange={(e) => setFormData(prev => ({ ...prev, primary_contact_name: e.target.value }))}
|
||
placeholder="John Smith"
|
||
className="mt-2"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<Label htmlFor="contact_email">
|
||
Contact Email <span className="text-red-500">*</span>
|
||
</Label>
|
||
<Input
|
||
id="contact_email"
|
||
type="email"
|
||
value={formData.primary_contact_email}
|
||
onChange={(e) => setFormData(prev => ({ ...prev, primary_contact_email: e.target.value }))}
|
||
placeholder="john@abcstaffing.com"
|
||
className="mt-2"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<Label htmlFor="contact_phone">Contact Phone <span className="text-red-500">*</span></Label>
|
||
<Input
|
||
id="contact_phone"
|
||
value={formData.primary_contact_phone}
|
||
onChange={(e) => setFormData(prev => ({ ...prev, primary_contact_phone: e.target.value }))}
|
||
placeholder="(555) 123-4567"
|
||
className="mt-2"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Google Address Autocomplete - SERVICE ADDRESS FIRST */}
|
||
<div>
|
||
<GoogleAddressInput
|
||
value={formData.service_address}
|
||
onChange={(value) => setFormData(prev => ({ ...prev, service_address: value }))}
|
||
placeholder="123 Main Street, Suite 100, San Francisco, CA 94105"
|
||
label="Service Address"
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
{/* Same as service checkbox */}
|
||
<div className="flex items-center gap-2">
|
||
<Checkbox
|
||
id="same_as_service"
|
||
checked={formData.same_as_billing}
|
||
onCheckedChange={(checked) => setFormData(prev => ({ ...prev, same_as_billing: checked }))}
|
||
className="data-[state=checked]:bg-[#0A39DF] data-[state=checked]:border-[#0A39DF]"
|
||
/>
|
||
<Label htmlFor="same_as_service" className="text-sm cursor-pointer">
|
||
Billing address is same as service address
|
||
</Label>
|
||
</div>
|
||
|
||
{/* Billing Address - only show if checkbox is unchecked */}
|
||
{!formData.same_as_billing && (
|
||
<div>
|
||
<GoogleAddressInput
|
||
value={formData.billing_address}
|
||
onChange={(value) => setFormData(prev => ({ ...prev, billing_address: value }))}
|
||
placeholder="456 Office Blvd, Oakland, CA 94612"
|
||
label="Billing Address"
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
{/* NEW FIELDS: Staff Count & Software */}
|
||
<div className="border-t border-slate-200 pt-6 mt-6">
|
||
<h3 className="font-semibold text-lg mb-4 flex items-center gap-2">
|
||
<Users className="w-5 h-5 text-[#0A39DF]" />
|
||
Operations Information
|
||
</h3>
|
||
|
||
<div className="grid grid-cols-2 gap-4">
|
||
{/* Total Employees */}
|
||
<div>
|
||
<Label htmlFor="total_employees">
|
||
How many staff members do you have? <span className="text-red-500">*</span>
|
||
</Label>
|
||
<Input
|
||
id="total_employees"
|
||
type="number"
|
||
value={formData.total_employees}
|
||
onChange={(e) => setFormData(prev => ({ ...prev, total_employees: e.target.value }))}
|
||
placeholder="e.g., 250"
|
||
className="mt-2"
|
||
/>
|
||
<p className="text-xs text-slate-500 mt-1">Total active workforce members</p>
|
||
</div>
|
||
|
||
{/* Has Software */}
|
||
<div>
|
||
<Label htmlFor="has_software">
|
||
Do you use operational software? <span className="text-red-500">*</span>
|
||
</Label>
|
||
<Select value={formData.has_software} onValueChange={(value) => {
|
||
setFormData(prev => ({
|
||
...prev,
|
||
has_software: value,
|
||
software_name: value === "no" ? "traditional" : ""
|
||
}));
|
||
}}>
|
||
<SelectTrigger className="mt-2">
|
||
<SelectValue placeholder="Select an option" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="yes">Yes, we have software</SelectItem>
|
||
<SelectItem value="no">No, traditional operations</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Software Details */}
|
||
{formData.has_software === "yes" && (
|
||
<div className="grid grid-cols-2 gap-4 mt-4">
|
||
<div>
|
||
<Label htmlFor="software_type">
|
||
Software Type <span className="text-red-500">*</span>
|
||
</Label>
|
||
<Select value={formData.software_type} onValueChange={(value) => setFormData(prev => ({ ...prev, software_type: value }))}>
|
||
<SelectTrigger className="mt-2">
|
||
<SelectValue placeholder="Select type" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="platform">Third-party Platform</SelectItem>
|
||
<SelectItem value="internal">Internal/Custom System</SelectItem>
|
||
<SelectItem value="building">Building on KROW</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
|
||
<div>
|
||
<Label htmlFor="software_name">
|
||
Software/Platform Name <span className="text-red-500">*</span>
|
||
</Label>
|
||
<Input
|
||
id="software_name"
|
||
value={formData.software_name}
|
||
onChange={(e) => setFormData(prev => ({ ...prev, software_name: e.target.value }))}
|
||
placeholder={formData.software_type === "internal" ? "Internal System" : "e.g., Instawork, Qwick"}
|
||
className="mt-2"
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
|
||
{/* STEP 4: Documents & Validation (was Step 3) */}
|
||
{currentStep === 4 && (
|
||
<Card className="border-2 border-slate-200">
|
||
<CardHeader className="bg-gradient-to-r from-slate-50 to-blue-50 border-b">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-12 h-12 bg-[#0A39DF] rounded-xl flex items-center justify-center">
|
||
<Shield className="w-6 h-6 text-white" />
|
||
</div>
|
||
<div>
|
||
<CardTitle className="text-2xl">Documents & Validation</CardTitle>
|
||
<p className="text-sm text-slate-600 mt-1">Compliance essentials - AI powered verification</p>
|
||
</div>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent className="p-6 space-y-6">
|
||
{/* W-9 Upload */}
|
||
<div>
|
||
<div className="flex items-start justify-between mb-4">
|
||
<div>
|
||
<h3 className="font-semibold flex items-center gap-2">
|
||
<FileText className="w-5 h-5 text-[#0A39DF]" />
|
||
W-9 Tax Form <span className="text-red-500">*</span>
|
||
</h3>
|
||
<p className="text-sm text-slate-600 mt-1">Federal tax classification document</p>
|
||
</div>
|
||
{docValidation.w9 && (
|
||
<Badge className={docValidation.w9.isValid ? "bg-green-100 text-green-700" : "bg-blue-100 text-blue-700"}>
|
||
{docValidation.w9.isValid ? "✅ Validated" : "ℹ Review"}
|
||
</Badge>
|
||
)}
|
||
</div>
|
||
|
||
<DragDropFileUpload
|
||
onFileSelect={(file) => handleFileUpload(file, 'w9')}
|
||
accept=".pdf,.jpg,.jpeg,.png"
|
||
uploading={uploadingDoc === 'w9' || validatingDoc === 'w9'}
|
||
uploaded={!!formData.w9_url}
|
||
uploadedFileName={formData.w9_file?.name}
|
||
disabled={uploadingDoc === 'w9' || validatingDoc === 'w9'}
|
||
/>
|
||
|
||
{docValidation.w9 && (
|
||
<div className="mt-3 p-3 bg-slate-50 rounded-lg">
|
||
<p className="text-sm font-medium text-slate-700 mb-1">Validation Results:</p>
|
||
{docValidation.w9.businessName && (
|
||
<p className="text-xs text-slate-600">• Business: {docValidation.w9.businessName}</p>
|
||
)}
|
||
{docValidation.w9.issues && docValidation.w9.issues.length > 0 && (
|
||
<div className="mt-2">
|
||
{docValidation.w9.issues.map((issue, idx) => (
|
||
<p key={idx} className="text-xs text-blue-600">ℹ {issue}</p>
|
||
))}
|
||
</div>
|
||
)}
|
||
{docValidation.w9.notes && (
|
||
<p className="text-xs text-slate-500 mt-2 italic">{docValidation.w9.notes}</p>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* COI Upload */}
|
||
<div>
|
||
<div className="flex items-start justify-between mb-4">
|
||
<div>
|
||
<h3 className="font-semibold flex items-center gap-2">
|
||
<Shield className="w-5 h-5 text-[#0A39DF]" />
|
||
Certificate of Insurance (COI) <span className="text-red-500">*</span>
|
||
</h3>
|
||
<p className="text-sm text-slate-600 mt-1">General liability insurance certificate</p>
|
||
</div>
|
||
{docValidation.coi && (
|
||
<Badge className={docValidation.coi.isValid ? "bg-green-100 text-green-700" : "bg-blue-100 text-blue-700"}>
|
||
{docValidation.coi.isValid ? "✅ Validated" : "ℹ Review"}
|
||
</Badge>
|
||
)}
|
||
</div>
|
||
|
||
<DragDropFileUpload
|
||
onFileSelect={(file) => handleFileUpload(file, 'coi')}
|
||
accept=".pdf,.jpg,.jpeg,.png"
|
||
uploading={uploadingDoc === 'coi' || validatingDoc === 'coi'}
|
||
uploaded={!!formData.coi_url}
|
||
uploadedFileName={formData.coi_file?.name}
|
||
disabled={uploadingDoc === 'coi' || validatingDoc === 'coi'}
|
||
/>
|
||
|
||
{docValidation.coi && (
|
||
<div className="mt-3 p-3 bg-slate-50 rounded-lg">
|
||
<p className="text-sm font-medium text-slate-700 mb-1">Validation Results:</p>
|
||
{docValidation.coi.expiryDate && (
|
||
<p className="text-xs text-slate-600">• Expires: {docValidation.coi.expiryDate}</p>
|
||
)}
|
||
{docValidation.coi.issues && docValidation.coi.issues.length > 0 && (
|
||
<div className="mt-2">
|
||
{docValidation.coi.issues.map((issue, idx) => (
|
||
<p key={idx} className="text-xs text-blue-600">ℹ {issue}</p>
|
||
))}
|
||
</div>
|
||
)}
|
||
{docValidation.coi.notes && (
|
||
<p className="text-xs text-slate-500 mt-2 italic">{docValidation.coi.notes}</p>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Secretary of State Certificate */}
|
||
<div className="border-2 border-red-300 rounded-lg p-6 bg-red-50/30">
|
||
<div className="flex items-start justify-between mb-4">
|
||
<div>
|
||
<h3 className="font-semibold flex items-center gap-2">
|
||
<CheckCircle2 className="w-5 h-5 text-[#0A39DF]" />
|
||
Secretary of State Certificate <span className="text-red-500">*</span>
|
||
</h3>
|
||
<p className="text-sm text-slate-600 mt-1">Business registration verification - <strong className="text-red-600">Required</strong></p>
|
||
</div>
|
||
{docValidation.sos && (
|
||
<Badge className={docValidation.sos.isValid || (docValidation.sos.isRegistered && docValidation.sos.autoVerified) ? "bg-green-100 text-green-700" : "bg-blue-100 text-blue-700"}>
|
||
{(docValidation.sos.isValid || (docValidation.sos.isRegistered && docValidation.sos.autoVerified)) ? "✅ Validated" : "ℹ Review"}
|
||
</Badge>
|
||
)}
|
||
</div>
|
||
|
||
{/* Auto-Check Button - NOW REQUIRED */}
|
||
<div className="mb-4 p-4 bg-blue-50 border-2 border-blue-300 rounded-lg">
|
||
<div className="flex items-start justify-between gap-4">
|
||
<div className="flex-1">
|
||
<p className="text-sm font-semibold text-blue-900 mb-1">🔍 Automatic Verification - <span className="text-red-600">Required</span></p>
|
||
<p className="text-xs text-blue-700">
|
||
Click "Auto-Check" to verify your business registration with the Secretary of State. This is mandatory for approval.
|
||
</p>
|
||
</div>
|
||
<Button
|
||
type="button"
|
||
onClick={async () => {
|
||
if (!formData.legal_name || !formData.service_address) { // Changed to service_address
|
||
toast({
|
||
title: "Missing Information",
|
||
description: "Please fill in business name and service address first",
|
||
variant: "destructive"
|
||
});
|
||
return;
|
||
}
|
||
|
||
setValidatingDoc('sos');
|
||
try {
|
||
// Extract state from service address (using service_address now)
|
||
const addressParts = formData.service_address.split(',');
|
||
const stateZip = addressParts[addressParts.length - 1]?.trim() || '';
|
||
const state = stateZip.split(' ')[0];
|
||
|
||
const validation = await base44.integrations.Core.InvokeLLM({
|
||
prompt: `
|
||
Search public Secretary of State records for this business:
|
||
Business Name: ${formData.legal_name}
|
||
State: ${state}
|
||
Tax ID: ${formData.tax_id || 'Not provided'}
|
||
|
||
Verify:
|
||
1. Is this business actively registered with the Secretary of State?
|
||
2. What is the registration status (Active, Inactive, Suspended)?
|
||
3. What is the business entity type on file?
|
||
4. When was it registered?
|
||
5. Is the registration current and in good standing?
|
||
|
||
Use public Secretary of State databases and business registries.
|
||
Return accurate verification results.
|
||
`,
|
||
add_context_from_internet: true,
|
||
response_json_schema: {
|
||
type: "object",
|
||
properties: {
|
||
isValid: { type: "boolean" },
|
||
isRegistered: { type: "boolean" },
|
||
businessName: { type: "string" },
|
||
registrationState: { type: "string" },
|
||
registrationStatus: { type: "string" },
|
||
entityType: { type: "string" },
|
||
registrationDate: { type: "string" },
|
||
issues: { type: "array", items: { type: "string" } },
|
||
notes: { type: "string" }
|
||
}
|
||
}
|
||
});
|
||
|
||
setDocValidation(prev => ({
|
||
...prev,
|
||
sos: {
|
||
...validation,
|
||
autoVerified: true,
|
||
isValid: validation.isRegistered && (validation.registrationStatus === 'Active' || validation.registrationStatus === 'Good Standing')
|
||
}
|
||
}));
|
||
|
||
toast({
|
||
title: validation.isRegistered ? "✅ Registration Verified" : "⚠️ Verification Results",
|
||
description: validation.notes || "Check results below",
|
||
});
|
||
} catch (error) {
|
||
toast({
|
||
title: "Verification Failed",
|
||
description: "Unable to auto-verify. Please upload certificate manually.",
|
||
variant: "destructive"
|
||
});
|
||
} finally {
|
||
setValidatingDoc(null);
|
||
}
|
||
}}
|
||
disabled={validatingDoc === 'sos'}
|
||
className="bg-blue-600 hover:bg-blue-700 text-white whitespace-nowrap"
|
||
>
|
||
{validatingDoc === 'sos' ? (
|
||
<>
|
||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||
Checking...
|
||
</>
|
||
) : (
|
||
<>
|
||
<Search className="w-4 h-4 mr-2" />
|
||
Auto-Check
|
||
</>
|
||
)}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Show auto-verification results */}
|
||
{docValidation.sos?.autoVerified && (
|
||
<div className="mb-4 p-4 bg-slate-50 rounded-lg border border-slate-200">
|
||
<div className="flex items-center gap-2 mb-3">
|
||
{docValidation.sos.isRegistered ? (
|
||
<CheckCircle2 className="w-5 h-5 text-green-600" />
|
||
) : (
|
||
<AlertCircle className="w-5 h-5 text-blue-600" />
|
||
)}
|
||
<p className="font-semibold text-sm">
|
||
{docValidation.sos.isRegistered ? "✅ Automatically Verified" : "ℹ Verification Results"}
|
||
</p>
|
||
</div>
|
||
|
||
<div className="space-y-2 text-sm">
|
||
{docValidation.sos.businessName && (
|
||
<div className="flex justify-between">
|
||
<span className="text-slate-600">Business Name:</span>
|
||
<span className="font-medium">{docValidation.sos.businessName}</span>
|
||
</div>
|
||
)}
|
||
{docValidation.sos.registrationState && (
|
||
<div className="flex justify-between">
|
||
<span className="text-slate-600">State:</span>
|
||
<span className="font-medium">{docValidation.sos.registrationState}</span>
|
||
</div>
|
||
)}
|
||
{docValidation.sos.registrationStatus && (
|
||
<div className="flex justify-between">
|
||
<span className="text-slate-600">Status:</span>
|
||
<Badge className={docValidation.sos.registrationStatus === 'Active' || docValidation.sos.registrationStatus === 'Good Standing' ? 'bg-green-100 text-green-700' : 'bg-blue-100 text-blue-700'}>
|
||
{docValidation.sos.registrationStatus}
|
||
</Badge>
|
||
</div>
|
||
)}
|
||
{docValidation.sos.entityType && (
|
||
<div className="flex justify-between">
|
||
<span className="text-slate-600">EntityType:</span>
|
||
<span className="font-medium">{docValidation.sos.entityType}</span>
|
||
</div>
|
||
)}
|
||
{docValidation.sos.registrationDate && (
|
||
<div className="flex justify-between">
|
||
<span className="text-slate-600">Registered:</span>
|
||
<span className="font-medium">{docValidation.sos.registrationDate}</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{docValidation.sos.issues && docValidation.sos.issues.length > 0 && (
|
||
<div className="mt-3 pt-3 border-t border-slate-200">
|
||
<p className="text-xs font-semibold text-blue-700 mb-2">Issues Found:</p>
|
||
{docValidation.sos.issues.map((issue, idx) => (
|
||
<p key={idx} className="text-xs text-blue-600">ℹ {issue}</p>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{docValidation.sos.notes && (
|
||
<p className="text-xs text-slate-500 mt-3 italic">{docValidation.sos.notes}</p>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Manual Upload Option with Drag & Drop */}
|
||
<div className="border-t border-slate-200 pt-4 mt-4">
|
||
<p className="text-sm font-medium text-slate-700 mb-4">Or upload certificate manually for verification:</p>
|
||
|
||
<DragDropFileUpload
|
||
onFileSelect={(file) => handleFileUpload(file, 'sos')}
|
||
accept=".pdf,.jpg,.jpeg,.png"
|
||
uploading={uploadingDoc === 'sos' && !docValidation.sos?.autoVerified}
|
||
uploaded={!!formData.sos_url && !docValidation.sos?.autoVerified}
|
||
uploadedFileName={formData.sos_file?.name}
|
||
disabled={uploadingDoc === 'sos' || validatingDoc === 'sos'}
|
||
/>
|
||
|
||
{/* Display manual validation results if not auto-verified, or if auto-verified failed/was skipped and manual was used */}
|
||
{docValidation.sos && !docValidation.sos.autoVerified && (
|
||
<div className="mt-3 p-3 bg-slate-50 rounded-lg">
|
||
<p className="text-sm font-medium text-slate-700 mb-1">Document Validation Results:</p>
|
||
{docValidation.sos.businessName && (
|
||
<p className="text-xs text-slate-600">• Business: {docValidation.sos.businessName}</p>
|
||
)}
|
||
{docValidation.sos.registrationState && (
|
||
<p className="text-xs text-slate-600">• State: {docValidation.sos.registrationState}</p>
|
||
)}
|
||
{docValidation.sos.expiryDate && (
|
||
<p className="text-xs text-slate-600">• Current through: {docValidation.sos.expiryDate}</p>
|
||
)}
|
||
{docValidation.sos.issues && docValidation.sos.issues.length > 0 && (
|
||
<div className="mt-2">
|
||
{docValidation.sos.issues.map((issue, idx) => (
|
||
<p key={idx} className="text-xs text-blue-600">ℹ {issue}</p>
|
||
))}
|
||
</div>
|
||
)}
|
||
{docValidation.sos.notes && (
|
||
<p className="text-xs text-slate-500 mt-2 italic">{docValidation.sos.notes}</p>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="mt-4 p-3 bg-red-50 rounded-lg border border-red-200">
|
||
<p className="text-xs text-red-900 flex items-start gap-2 font-semibold">
|
||
<AlertCircle className="w-4 h-4 mt-0.5 flex-shrink-0" />
|
||
<span>
|
||
<strong>REQUIRED:</strong> This certificate verifies your business is actively registered with your state.
|
||
You must either use Auto-Check or upload a valid certificate to proceed.
|
||
</span>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* NEW: Compliance Attestations Section */}
|
||
<div className="border-2 border-blue-300 rounded-lg p-6 bg-blue-50/30 mt-6">
|
||
<div className="flex items-center gap-3 mb-4">
|
||
<div className="w-10 h-10 bg-blue-600 rounded-lg flex items-center justify-center">
|
||
<Shield className="w-5 h-5 text-white" />
|
||
</div>
|
||
<div>
|
||
<h3 className="font-semibold text-lg text-blue-900">Compliance Attestations</h3>
|
||
<p className="text-sm text-blue-700">Verify your compliance with employment and safety regulations</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="space-y-4 bg-white p-6 rounded-lg border-2 border-blue-200">
|
||
{/* Background Check Attestation */}
|
||
<div className="flex items-start gap-3 p-4 bg-slate-50 rounded-lg border border-slate-200">
|
||
<Checkbox
|
||
id="background_check"
|
||
checked={formData.background_check_attestation}
|
||
onCheckedChange={(checked) => setFormData(prev => ({ ...prev, background_check_attestation: checked }))}
|
||
className="mt-1 data-[state=checked]:bg-[#0A39DF] data-[state=checked]:border-[#0A39DF]"
|
||
/>
|
||
<div className="flex-1">
|
||
<Label htmlFor="background_check" className="font-semibold cursor-pointer text-slate-900">
|
||
Background Check Compliance <span className="text-red-500">*</span>
|
||
</Label>
|
||
<p className="text-sm text-slate-600 mt-1">
|
||
I attest that my company conducts background checks on all employees in accordance with federal, state, and local laws.
|
||
We maintain proper documentation and follow all FCRA (Fair Credit Reporting Act) requirements.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* I-9 Compliance Attestation */}
|
||
<div className="flex items-start gap-3 p-4 bg-slate-50 rounded-lg border border-slate-200">
|
||
<Checkbox
|
||
id="i9_compliance"
|
||
checked={formData.i9_compliance_attestation}
|
||
onCheckedChange={(checked) => setFormData(prev => ({ ...prev, i9_compliance_attestation: checked }))}
|
||
className="mt-1 data-[state=checked]:bg-[#0A39DF] data-[state=checked]:border-[#0A39DF]"
|
||
/>
|
||
<div className="flex-1">
|
||
<Label htmlFor="i9_compliance" className="font-semibold cursor-pointer text-slate-900">
|
||
I-9 Employment Eligibility Verification <span className="text-red-500">*</span>
|
||
</Label>
|
||
<p className="text-sm text-slate-600 mt-1">
|
||
I attest that my company maintains complete and valid I-9 forms for all employees as required by U.S. immigration law.
|
||
All employees are verified for employment eligibility and documentation is kept on file for the required retention period.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Legal Compliance Attestation */}
|
||
<div className="flex items-start gap-3 p-4 bg-slate-50 rounded-lg border border-slate-200">
|
||
<Checkbox
|
||
id="legal_compliance"
|
||
checked={formData.legal_compliance_attestation}
|
||
onCheckedChange={(checked) => setFormData(prev => ({ ...prev, legal_compliance_attestation: checked }))}
|
||
className="mt-1 data-[state=checked]:bg-[#0A39DF] data-[state=checked]:border-[#0A39DF]"
|
||
/>
|
||
<div className="flex-1">
|
||
<Label htmlFor="legal_compliance" className="font-semibold cursor-pointer text-slate-900">
|
||
General Legal and Regulatory Compliance <span className="text-red-500">*</span>
|
||
</Label>
|
||
<p className="text-sm text-slate-600 mt-1">
|
||
I attest that my company complies with all applicable federal, state, and local employment laws including but not limited to:
|
||
wage and hour laws, workers' compensation, unemployment insurance, anti-discrimination laws, OSHA safety standards,
|
||
and all licensing requirements for our industry and geographic locations.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Warning Message */}
|
||
{(!formData.background_check_attestation || !formData.i9_compliance_attestation || !formData.legal_compliance_attestation) && (
|
||
<div className="mt-4 p-4 bg-red-50 border-2 border-red-200 rounded-lg flex items-start gap-2">
|
||
<AlertCircle className="w-5 h-5 text-red-600 mt-0.5 flex-shrink-0" />
|
||
<div>
|
||
<p className="text-sm text-red-900 font-semibold">Required Attestations</p>
|
||
<p className="text-sm text-red-800 mt-1">
|
||
All compliance attestations are mandatory. By checking these boxes, you affirm that your company meets all legal requirements
|
||
and understands that providing false information may result in immediate termination of vendor partnership and potential legal action.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Success Message */}
|
||
{formData.background_check_attestation && formData.i9_compliance_attestation && formData.legal_compliance_attestation && (
|
||
<div className="mt-4 p-4 bg-green-50 border-2 border-green-200 rounded-lg flex items-center gap-2">
|
||
<CheckCircle2 className="w-5 h-5 text-green-600" />
|
||
<p className="text-sm text-green-900 font-medium">
|
||
✓ All compliance attestations completed. Your commitment to legal compliance is appreciated.
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
|
||
{/* STEP 5: Service Coverage (was Step 4) */}
|
||
{currentStep === 5 && (
|
||
<Card className="border-2 border-slate-200">
|
||
<CardHeader className="bg-gradient-to-r from-slate-50 to-blue-50 border-b">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-12 h-12 bg-[#0A39DF] rounded-xl flex items-center justify-center">
|
||
<MapPin className="w-6 h-6 text-white" />
|
||
</div>
|
||
<div>
|
||
<CardTitle className="text-2xl">Service Coverage</CardTitle>
|
||
<p className="text-sm text-slate-600 mt-1">Select all cities and regions where you can provide services</p>
|
||
</div>
|
||
</div>
|
||
<Badge className="bg-blue-100 text-blue-700 text-sm px-3 py-1">
|
||
{Object.values(formData.selected_cities).reduce((sum, cities) => sum + cities.length, 0)} Cities Selected
|
||
</Badge>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent className="p-6 space-y-6">
|
||
{/* Search and Quick Actions */}
|
||
<div className="flex gap-3">
|
||
<div className="relative flex-1">
|
||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||
<Input
|
||
placeholder="Search states or cities..."
|
||
className="pl-10"
|
||
onChange={(e) => {
|
||
// Filtering logic would be implemented here, e.g., storing search term in state
|
||
// and then filtering `Object.entries(usLocationData)` below.
|
||
// For now, it's a visual placeholder as per original outline intent.
|
||
}}
|
||
/>
|
||
</div>
|
||
<Button
|
||
variant="outline"
|
||
onClick={() => {
|
||
// Select all major metro areas
|
||
const majorMetros = {
|
||
"California": ["Los Angeles", "San Francisco", "San Diego", "San Jose"],
|
||
"New York": ["New York City"],
|
||
"Texas": ["Houston", "Dallas", "Austin", "San Antonio"],
|
||
"Illinois": ["Chicago"],
|
||
"Florida": ["Miami", "Orlando", "Tampa"]
|
||
};
|
||
setFormData(prev => ({ ...prev, selected_cities: majorMetros }));
|
||
}}
|
||
>
|
||
Select Major Metros
|
||
</Button>
|
||
</div>
|
||
|
||
{/* Selected Regions Summary */}
|
||
{Object.values(formData.selected_cities).reduce((sum, cities) => sum + cities.length, 0) > 0 && (
|
||
<div className="p-4 bg-green-50 border-2 border-green-200 rounded-lg">
|
||
<div className="flex items-center justify-between mb-3">
|
||
<h4 className="font-semibold text-green-900">Selected Service Areas</h4>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => setFormData(prev => ({ ...prev, selected_cities: {} }))}
|
||
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
||
>
|
||
Clear All
|
||
</Button>
|
||
</div>
|
||
<div className="space-y-2">
|
||
{Object.entries(formData.selected_cities).map(([state, cities]) => {
|
||
if (cities.length === 0) return null; // Don't show states with no selected cities
|
||
return (
|
||
<div key={state} className="flex flex-wrap gap-2 items-center">
|
||
<Badge className="bg-green-600 text-white font-semibold">
|
||
{state}
|
||
</Badge>
|
||
{cities.map(city => (
|
||
<button
|
||
key={city}
|
||
type="button"
|
||
onClick={() => {
|
||
setFormData(prev => {
|
||
const updatedStateCities = prev.selected_cities[state].filter(c => c !== city);
|
||
if (updatedStateCities.length === 0) {
|
||
const newStateCities = { ...prev.selected_cities };
|
||
delete newStateCities[state];
|
||
return { ...prev, selected_cities: newStateCities };
|
||
}
|
||
return {
|
||
...prev,
|
||
selected_cities: {
|
||
...prev.selected_cities,
|
||
[state]: updatedStateCities
|
||
}
|
||
};
|
||
});
|
||
}}
|
||
className="px-2 py-1 rounded-full text-xs font-medium bg-white border border-slate-300 text-slate-700 flex items-center gap-1 hover:bg-slate-50 transition-colors"
|
||
>
|
||
{city}
|
||
<Trash2 className="w-3 h-3 text-red-500" />
|
||
</button>
|
||
))}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* State and City Selection */}
|
||
<div className="border-2 border-slate-200 rounded-lg p-6 max-h-[500px] overflow-y-auto">
|
||
<div className="space-y-6">
|
||
{Object.entries(usLocationData).map(([state, cities]) => (
|
||
<div key={state} className="border-b border-slate-200 pb-4 last:border-0">
|
||
<div className="flex items-center justify-between mb-3">
|
||
<h4 className="font-bold text-lg text-slate-900">{state}</h4>
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => {
|
||
const isAllSelected = formData.selected_cities[state]?.length === cities.length;
|
||
setFormData(prev => {
|
||
const newStateCities = { ...prev.selected_cities };
|
||
if (isAllSelected) {
|
||
delete newStateCities[state]; // If all are selected, deselect all and remove state entry
|
||
} else {
|
||
newStateCities[state] = cities; // Select all cities for this state
|
||
}
|
||
return { ...prev, selected_cities: newStateCities };
|
||
});
|
||
}}
|
||
className="text-xs"
|
||
>
|
||
{formData.selected_cities[state]?.length === cities.length ? "Deselect All" : "Select All"}
|
||
</Button>
|
||
</div>
|
||
<div className="flex flex-wrap gap-2">
|
||
{cities.map(city => {
|
||
const isSelected = formData.selected_cities[state]?.includes(city);
|
||
return (
|
||
<button
|
||
key={city}
|
||
type="button"
|
||
onClick={() => {
|
||
setFormData(prev => {
|
||
const stateCities = prev.selected_cities[state] || [];
|
||
const newCities = isSelected
|
||
? stateCities.filter(c => c !== city)
|
||
: [...stateCities, city];
|
||
|
||
const newStateCities = { ...prev.selected_cities };
|
||
if (newCities.length === 0) {
|
||
delete newStateCities[state];
|
||
} else {
|
||
newStateCities[state] = newCities;
|
||
}
|
||
|
||
return {
|
||
...prev,
|
||
selected_cities: newStateCities
|
||
};
|
||
});
|
||
}}
|
||
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-all ${
|
||
isSelected
|
||
? 'bg-[#0A39DF] text-white shadow-md'
|
||
: 'bg-slate-100 text-slate-700 hover:bg-slate-200'
|
||
}`}
|
||
>
|
||
{isSelected && <CheckCircle2 className="w-3 h-3 inline mr-1" />}
|
||
{city}
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{Object.values(formData.selected_cities).reduce((sum, cities) => sum + cities.length, 0) === 0 && (
|
||
<div className="text-center py-8 text-slate-500">
|
||
<MapPin className="w-12 h-12 mx-auto mb-3 text-slate-300" />
|
||
<p className="font-medium">No cities selected yet</p>
|
||
<p className="text-sm">Select the cities where you can provide staffing services</p>
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
|
||
{/* STEP 6: Rate Proposals (was Step 5) */}
|
||
{currentStep === 6 && (
|
||
<Card className="border-2 border-slate-200">
|
||
<CardHeader className="bg-gradient-to-r from-slate-50 to-blue-50 border-b">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-12 h-12 bg-[#0A39DF] rounded-xl flex items-center justify-center">
|
||
<DollarSign className="w-6 h-6 text-white" />
|
||
</div>
|
||
<div>
|
||
<CardTitle className="text-2xl">Service Matrix Builder</CardTitle>
|
||
<p className="text-sm text-slate-600 mt-1">Tell us where you shine - AI-powered rate analysis</p>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-4">
|
||
<div className="text-right">
|
||
<p className="text-xs text-slate-500">Vendor Admin Fee (VA)</p>
|
||
<p className="text-xl font-bold text-purple-600">{invite?.vendor_admin_fee || 12}%</p>
|
||
<p className="text-[10px] text-slate-400">Set by Procurement</p>
|
||
</div>
|
||
<Button
|
||
type="button"
|
||
onClick={() => {
|
||
// Use service_address for minimum wage calculation
|
||
const localMinWage = getMinimumWageFromAddress(formData.service_address);
|
||
const newRole = {
|
||
role_name: "Custom Role",
|
||
category: "Event Staff",
|
||
employee_wage: localMinWage || 18,
|
||
markup_percentage: 20,
|
||
vendor_fee_percentage: invite?.vendor_admin_fee || 12,
|
||
client_rate: 0,
|
||
is_active: true,
|
||
ai_analysis: null,
|
||
is_custom: true,
|
||
location_rates: {}
|
||
};
|
||
const vendorFee = invite?.vendor_admin_fee || 12;
|
||
newRole.client_rate = parseFloat((newRole.employee_wage * (1 + newRole.markup_percentage / 100) * (1 + vendorFee / 100)).toFixed(2));
|
||
|
||
setFormData(prev => ({
|
||
...prev,
|
||
rate_proposals: [...prev.rate_proposals, newRole]
|
||
}));
|
||
|
||
toast({
|
||
title: "Custom Role Added",
|
||
description: "New custom role added to rate proposals"
|
||
});
|
||
}}
|
||
className="bg-[#0A39DF] hover:bg-[#0A39DF]/90"
|
||
>
|
||
<Plus className="w-4 h-4 mr-2" />
|
||
Add Custom Role
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent className="p-6">
|
||
<div className="mb-6 p-4 bg-gradient-to-r from-blue-50 to-purple-50 border border-blue-200 rounded-lg">
|
||
<div className="flex items-start gap-3">
|
||
<Sparkles className="w-5 h-5 text-purple-600 mt-0.5" />
|
||
<div className="flex-1">
|
||
<p className="text-sm font-semibold text-slate-900 mb-1">🧠 AI runs in background:</p>
|
||
<p className="text-xs text-slate-700">
|
||
Benchmarks each entry against KROW's dynamic rate matrix, historic client spend,
|
||
and market average for your region and role.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Minimum Wage Detected */}
|
||
{formData.service_address && ( // Changed to service_address
|
||
<div className="mb-6 p-4 bg-green-50 border-2 border-green-200 rounded-lg">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-3">
|
||
<MapPin className="w-5 h-5 text-green-600" />
|
||
<div>
|
||
<p className="text-sm font-semibold text-green-900">
|
||
Local Minimum Wage Detected: ${getMinimumWageFromAddress(formData.service_address)}/hr
|
||
</p>
|
||
<p className="text-xs text-green-700 mt-1">
|
||
Based on your business location. All payrates are pre-populated at or above this minimum. You can adjust them as needed.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<Badge className="bg-green-600 text-white text-sm px-3 py-1">
|
||
Auto-Detected
|
||
</Badge>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Global Controls */}
|
||
<div className="mb-6 p-4 bg-slate-50 border-2 border-slate-300 rounded-lg">
|
||
<div className="flex items-center justify-between gap-6">
|
||
<div className="flex items-center gap-4 flex-1">
|
||
<h4 className="font-semibold text-slate-900">Quick Actions:</h4>
|
||
|
||
{/* Select All / Deselect All */}
|
||
<Button
|
||
type="button"
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => {
|
||
const allActive = formData.rate_proposals.every(r => r.is_active);
|
||
const newRates = formData.rate_proposals.map(r => ({
|
||
...r,
|
||
is_active: !allActive
|
||
}));
|
||
setFormData(prev => ({ ...prev, rate_proposals: newRates }));
|
||
}}
|
||
className="border-[#0A39DF] text-[#0A39DF] hover:bg-blue-50"
|
||
>
|
||
{formData.rate_proposals.every(r => r.is_active) ? (
|
||
<>
|
||
<CheckCircle2 className="w-4 h-4 mr-2" />
|
||
Deselect All
|
||
</>
|
||
) : (
|
||
<>
|
||
<CheckCircle2 className="w-4 h-4 mr-2" />
|
||
Select All
|
||
</>
|
||
)}
|
||
</Button>
|
||
</div>
|
||
|
||
{/* Apply Markup to All */}
|
||
<div className="flex items-center gap-3">
|
||
<Label htmlFor="global_markup" className="text-sm font-semibold whitespace-nowrap">
|
||
Markup:
|
||
</Label>
|
||
<div className="flex items-center gap-2">
|
||
<Input
|
||
id="global_markup"
|
||
type="number"
|
||
step="0.1"
|
||
placeholder="e.g., 20"
|
||
className="w-24 text-center font-semibold"
|
||
/>
|
||
<span className="text-sm text-slate-500">%</span>
|
||
<Button
|
||
type="button"
|
||
size="sm"
|
||
onClick={() => {
|
||
const globalMarkupInput = document.getElementById('global_markup');
|
||
const globalMarkup = parseFloat(globalMarkupInput.value);
|
||
|
||
if (isNaN(globalMarkup) || globalMarkup <= 0) {
|
||
toast({
|
||
title: "Invalid Markup",
|
||
description: "Please enter a valid markup percentage",
|
||
variant: "destructive"
|
||
});
|
||
return;
|
||
}
|
||
|
||
const vendorFee = invite?.vendor_admin_fee || 12;
|
||
const newRates = formData.rate_proposals.map(rate => ({
|
||
...rate,
|
||
markup_percentage: globalMarkup,
|
||
client_rate: parseFloat((rate.employee_wage * (1 + globalMarkup / 100) * (1 + vendorFee / 100)).toFixed(2)),
|
||
location_rates: Object.fromEntries(
|
||
Object.entries(rate.location_rates || {}).map(([loc, lr]) => [
|
||
loc,
|
||
{
|
||
...lr,
|
||
markup_percentage: globalMarkup,
|
||
client_rate: parseFloat((lr.employee_wage * (1 + globalMarkup / 100) * (1 + vendorFee / 100)).toFixed(2))
|
||
}
|
||
])
|
||
)
|
||
}));
|
||
|
||
setFormData(prev => ({ ...prev, rate_proposals: newRates }));
|
||
|
||
toast({
|
||
title: "Markup Applied",
|
||
description: `${newRates.length} roles updated with ${globalMarkup}% markup`,
|
||
});
|
||
}}
|
||
className="bg-green-600 hover:bg-green-700 text-white"
|
||
>
|
||
Apply
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Enhanced Rate Cards with Location Support */}
|
||
<div className="space-y-3">
|
||
{(() => {
|
||
// Get minimum wage based on business location (service_address)
|
||
const localMinWage = getMinimumWageFromAddress(formData.service_address);
|
||
|
||
// Pre-defined roles with wages based on local minimum wage
|
||
const defaultRoles = [
|
||
{ category: "Bartending", role: "Bartender", wageMultiplier: 1.2 },
|
||
{ category: "Kitchen and Culinary", role: "Cook", wageMultiplier: 1.2 },
|
||
{ category: "Kitchen and Culinary", role: "Line Cook", wageMultiplier: 1.2 },
|
||
{ category: "Kitchen and Culinary", role: "Sous Chef", wageMultiplier: 1.6 },
|
||
{ category: "Kitchen and Culinary", role: "Executive Chef", wageMultiplier: 2.3 },
|
||
{ category: "Kitchen and Culinary", role: "Prep Cook", wageMultiplier: 1.0 },
|
||
{ category: "Concessions", role: "Concessions Attendant", wageMultiplier: 1.0 },
|
||
{ category: "Concessions", role: "Cashier", wageMultiplier: 1.0 },
|
||
{ category: "Facilities", role: "Janitor", wageMultiplier: 1.05 },
|
||
{ category: "Facilities", role: "Maintenance Technician", wageMultiplier: 1.4 },
|
||
{ category: "Bartending", role: "Bar Back", wageMultiplier: 1.0 },
|
||
{ category: "Security", role: "Security Guard", wageMultiplier: 1.3 },
|
||
{ category: "Event Staff", role: "Event Server", wageMultiplier: 1.05 },
|
||
{ category: "Event Staff", role: "Event Coordinator", wageMultiplier: 1.6 },
|
||
{ category: "Management", role: "General Manager", wageMultiplier: 2.6 },
|
||
{ category: "Management", role: "Assistant Manager", wageMultiplier: 1.8 },
|
||
];
|
||
|
||
// Initialize rate proposals if empty
|
||
if (formData.rate_proposals.length === 0) {
|
||
const initialRates = defaultRoles.map(r => {
|
||
const calculatedWage = parseFloat((localMinWage * r.wageMultiplier).toFixed(2));
|
||
const vendorFee = invite?.vendor_admin_fee || 12;
|
||
return {
|
||
category: r.category,
|
||
role_name: r.role,
|
||
employee_wage: calculatedWage,
|
||
markup_percentage: 20,
|
||
vendor_fee_percentage: vendorFee,
|
||
client_rate: parseFloat((calculatedWage * 1.2 * (1 + (vendorFee / 100))).toFixed(2)),
|
||
is_active: true,
|
||
ai_analysis: null,
|
||
is_custom: false,
|
||
location_rates: {} // Initialize empty object for location-specific rates
|
||
};
|
||
});
|
||
// Using a functional update to ensure we get the latest state
|
||
setFormData(prev => ({ ...prev, rate_proposals: initialRates }));
|
||
}
|
||
return null; // This IIFE does not render anything directly
|
||
})()}
|
||
|
||
{formData.rate_proposals.map((rate, idx) => {
|
||
const vendorFee = invite?.vendor_admin_fee || 12;
|
||
const selectedLocations = Object.entries(formData.selected_cities).flatMap(([state, cities]) =>
|
||
cities.map(city => `${city}, ${state}`)
|
||
);
|
||
|
||
// Determine status based on client rate vs market average (simulated)
|
||
const marketAverage = rate.employee_wage * 1.35;
|
||
let status = "competitive";
|
||
let statusIcon = "✓";
|
||
let statusText = "Good Deal";
|
||
let statusClass = "bg-green-100 text-green-700";
|
||
|
||
if (rate.client_rate > marketAverage * 1.15) {
|
||
status = "risk";
|
||
statusIcon = "!";
|
||
statusText = "Risk Zone";
|
||
statusClass = "bg-red-100 text-red-700";
|
||
} else if (rate.client_rate > marketAverage * 1.05) {
|
||
status = "market";
|
||
statusIcon = "~";
|
||
statusText = "At Market";
|
||
statusClass = "bg-blue-100 text-blue-700";
|
||
}
|
||
|
||
return (
|
||
<Card key={idx} className={`border-2 transition-all ${rate.is_active ? 'border-slate-200 hover:border-[#0A39DF]' : 'border-slate-100 bg-slate-50 opacity-60'}`}>
|
||
<CardContent className="p-4">
|
||
<div className="flex items-start gap-4">
|
||
{/* Toggle */}
|
||
<div className="flex items-center pt-2">
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
const newRates = [...formData.rate_proposals];
|
||
newRates[idx].is_active = !newRates[idx].is_active;
|
||
setFormData(prev => ({ ...prev, rate_proposals: newRates }));
|
||
}}
|
||
className={`w-12 h-6 rounded-full transition-colors relative ${rate.is_active ? 'bg-green-500' : 'bg-slate-300'}`}
|
||
>
|
||
<div className={`absolute top-1 w-4 h-4 bg-white rounded-full transition-transform ${rate.is_active ? 'translate-x-7' : 'translate-x-1'}`}></div>
|
||
</button>
|
||
</div>
|
||
|
||
{/* Role Info */}
|
||
<div className="flex-1 grid grid-cols-6 gap-3 items-center">
|
||
{/* Position */}
|
||
<div>
|
||
<p className="text-xs text-slate-500 mb-1">Position</p>
|
||
{rate.is_custom ? (
|
||
<Input
|
||
value={rate.role_name}
|
||
onChange={(e) => {
|
||
const newRates = [...formData.rate_proposals];
|
||
newRates[idx].role_name = e.target.value;
|
||
setFormData(prev => ({ ...prev, rate_proposals: newRates }));
|
||
}}
|
||
className="h-8 text-sm"
|
||
disabled={!rate.is_active}
|
||
/>
|
||
) : (
|
||
<p className="font-semibold text-slate-900 text-sm">{rate.role_name}</p>
|
||
)}
|
||
<p className="text-[10px] text-slate-400">{rate.category}</p>
|
||
</div>
|
||
|
||
{/* Payrate */}
|
||
<div>
|
||
<p className="text-xs text-slate-500 mb-1">Payrate</p>
|
||
<div className="relative">
|
||
<span className="absolute left-2 top-1/2 -translate-y-1/2 text-xs text-slate-400">$</span>
|
||
<Input
|
||
type="number"
|
||
step="0.01"
|
||
value={rate.employee_wage}
|
||
onChange={(e) => {
|
||
const newRates = [...formData.rate_proposals];
|
||
const wage = parseFloat(e.target.value) || 0;
|
||
newRates[idx].employee_wage = wage;
|
||
newRates[idx].client_rate = parseFloat((wage * (1 + newRates[idx].markup_percentage / 100) * (1 + vendorFee / 100)).toFixed(2));
|
||
// Also update client rates in location_rates
|
||
if (newRates[idx].location_rates) {
|
||
newRates[idx].location_rates = Object.fromEntries(
|
||
Object.entries(newRates[idx].location_rates).map(([loc, lr]) => [
|
||
loc,
|
||
{
|
||
...lr,
|
||
employee_wage: wage, // Update wage for location-specific rate
|
||
client_rate: parseFloat((wage * (1 + lr.markup_percentage / 100) * (1 + vendorFee / 100)).toFixed(2))
|
||
}
|
||
])
|
||
);
|
||
}
|
||
setFormData(prev => ({ ...prev, rate_proposals: newRates }));
|
||
}}
|
||
className="h-8 text-sm pl-5 font-semibold"
|
||
disabled={!rate.is_active}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Markup */}
|
||
<div>
|
||
<p className="text-xs text-slate-500 mb-1">Mark up</p>
|
||
<div className="relative">
|
||
<Input
|
||
type="number"
|
||
step="0.1"
|
||
value={rate.markup_percentage}
|
||
onChange={(e) => {
|
||
const newRates = [...formData.rate_proposals];
|
||
const markup = parseFloat(e.target.value) || 0;
|
||
newRates[idx].markup_percentage = markup;
|
||
newRates[idx].client_rate = parseFloat((newRates[idx].employee_wage * (1 + markup / 100) * (1 + vendorFee / 100)).toFixed(2));
|
||
// Also update client rates in location_rates
|
||
if (newRates[idx].location_rates) {
|
||
newRates[idx].location_rates = Object.fromEntries(
|
||
Object.entries(newRates[idx].location_rates).map(([loc, lr]) => [
|
||
loc,
|
||
{
|
||
...lr,
|
||
markup_percentage: markup, // Update markup for location-specific rate
|
||
client_rate: parseFloat((lr.employee_wage * (1 + markup / 100) * (1 + vendorFee / 100)).toFixed(2))
|
||
}
|
||
])
|
||
);
|
||
}
|
||
setFormData(prev => ({ ...prev, rate_proposals: newRates }));
|
||
}}
|
||
className="h-8 text-sm font-semibold text-blue-600"
|
||
disabled={!rate.is_active}
|
||
/>
|
||
<span className="absolute right-2 top-1/2 -translate-y-1/2 text-xs text-slate-400">%</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Vendor Fee % (Read-only) */}
|
||
<div>
|
||
<p className="text-xs text-slate-500 mb-1">Vendor Fee %</p>
|
||
<div className="h-8 flex items-center justify-center bg-purple-50 border border-purple-200 rounded text-sm font-bold text-purple-600">
|
||
{vendorFee}%
|
||
</div>
|
||
</div>
|
||
|
||
{/* Proposed Bill Rate */}
|
||
<div>
|
||
<p className="text-xs text-slate-500 mb-1">Proposed Bill Rate</p>
|
||
<div className="h-8 flex items-center justify-center bg-green-50 border border-green-200 rounded">
|
||
<span className="text-sm font-bold text-green-700">${rate.client_rate}</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Status */}
|
||
<div>
|
||
<p className="text-xs text-slate-500 mb-1">Status</p>
|
||
<Badge className={`${statusClass} text-xs font-semibold`}>
|
||
{statusIcon} {statusText}
|
||
</Badge>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Actions */}
|
||
{rate.is_custom && (
|
||
<Button
|
||
type="button"
|
||
variant="ghost"
|
||
size="icon"
|
||
onClick={() => {
|
||
setFormData(prev => ({
|
||
...prev,
|
||
rate_proposals: prev.rate_proposals.filter((_, i) => i !== idx)
|
||
}));
|
||
}}
|
||
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
||
>
|
||
<Trash2 className="w-4 h-4" />
|
||
</Button>
|
||
)}
|
||
</div>
|
||
|
||
{/* NEW: Location-Specific Rates Section */}
|
||
{rate.is_active && selectedLocations.length > 0 && ( // Show if active and at least one location is selected
|
||
<div className="mt-4 pt-4 border-t border-slate-200">
|
||
<div className="flex items-center justify-between mb-3">
|
||
<h5 className="font-semibold text-sm">Location-Specific Rates</h5>
|
||
<Badge variant="outline" className="text-xs">
|
||
{selectedLocations.length} Locations Available
|
||
</Badge>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||
{selectedLocations.map(location => {
|
||
// Default to the base rate if no specific location rate is set
|
||
const locationRate = rate.location_rates?.[location] || {
|
||
employee_wage: rate.employee_wage,
|
||
markup_percentage: rate.markup_percentage,
|
||
client_rate: rate.client_rate
|
||
};
|
||
|
||
return (
|
||
<div key={location} className="p-3 bg-slate-50 rounded-lg border border-slate-200">
|
||
<p className="text-xs font-semibold text-slate-700 mb-2 truncate">{location}</p>
|
||
<div className="grid grid-cols-3 gap-2">
|
||
<div>
|
||
<Label className="text-[10px]">Payrate</Label>
|
||
<div className="relative">
|
||
<span className="absolute left-1.5 top-1/2 -translate-y-1/2 text-[10px]">$</span>
|
||
<Input
|
||
type="number"
|
||
step="0.01"
|
||
value={locationRate.employee_wage}
|
||
onChange={(e) => {
|
||
const newWage = parseFloat(e.target.value) || 0;
|
||
const newClientRate = parseFloat((newWage * (1 + locationRate.markup_percentage / 100) * (1 + vendorFee / 100)).toFixed(2));
|
||
|
||
setFormData(prev => {
|
||
const newRates = [...prev.rate_proposals];
|
||
if (!newRates[idx].location_rates) newRates[idx].location_rates = {};
|
||
newRates[idx].location_rates[location] = {
|
||
...locationRate,
|
||
employee_wage: newWage,
|
||
client_rate: newClientRate
|
||
};
|
||
return { ...prev, rate_proposals: newRates };
|
||
});
|
||
}}
|
||
className="h-7 text-xs pl-4"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<Label className="text-[10px]">Markup</Label>
|
||
<Input
|
||
type="number"
|
||
step="0.1"
|
||
value={locationRate.markup_percentage}
|
||
onChange={(e) => {
|
||
const newMarkup = parseFloat(e.target.value) || 0;
|
||
const newClientRate = parseFloat((locationRate.employee_wage * (1 + newMarkup / 100) * (1 + vendorFee / 100)).toFixed(2));
|
||
|
||
setFormData(prev => {
|
||
const newRates = [...prev.rate_proposals];
|
||
if (!newRates[idx].location_rates) newRates[idx].location_rates = {};
|
||
newRates[idx].location_rates[location] = {
|
||
...locationRate,
|
||
markup_percentage: newMarkup,
|
||
client_rate: newClientRate
|
||
};
|
||
return { ...prev, rate_proposals: newRates };
|
||
});
|
||
}}
|
||
className="h-7 text-xs"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<Label className="text-[10px]">Bill Rate</Label>
|
||
<div className="h-7 flex items-center justify-center bg-green-50 border border-green-200 rounded text-xs font-bold text-green-700">
|
||
${locationRate.client_rate}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
<Button
|
||
type="button"
|
||
variant="outline"
|
||
size="sm"
|
||
className="mt-3 w-full text-xs border-slate-300 hover:bg-slate-50"
|
||
onClick={() => {
|
||
// Copy base rate to all locations
|
||
setFormData(prev => {
|
||
const newRates = [...prev.rate_proposals];
|
||
newRates[idx].location_rates = {};
|
||
selectedLocations.forEach(location => {
|
||
newRates[idx].location_rates[location] = {
|
||
employee_wage: rate.employee_wage,
|
||
markup_percentage: rate.markup_percentage,
|
||
client_rate: rate.client_rate
|
||
};
|
||
});
|
||
return { ...prev, rate_proposals: newRates };
|
||
});
|
||
toast({
|
||
title: "Rates Copied",
|
||
description: "Base rate applied to all locations"
|
||
});
|
||
}}
|
||
>
|
||
Apply Base Rate to All Locations
|
||
</Button>
|
||
</div>
|
||
)}
|
||
|
||
{/* Expanded Details - Visual Breakdown */}
|
||
{rate.is_active && (
|
||
<div className="mt-4 pt-4 border-t border-slate-200">
|
||
<div className="grid grid-cols-3 gap-4">
|
||
{/* Cost Breakdown Bar */}
|
||
<div className="col-span-2">
|
||
<p className="text-xs font-semibold text-slate-700 mb-2">Cost Breakdown</p>
|
||
<div className="flex items-center gap-2 mb-2">
|
||
<div className="flex-1 bg-slate-200 rounded-full h-8 overflow-hidden flex text-xs font-semibold">
|
||
<div
|
||
className="bg-green-500 flex items-center justify-center text-white"
|
||
style={{ width: `${(rate.employee_wage / rate.client_rate) * 100}%` }}
|
||
title={`Payrate: $${rate.employee_wage}`}
|
||
>
|
||
${rate.employee_wage}
|
||
</div>
|
||
<div
|
||
className="bg-blue-500 flex items-center justify-center text-white"
|
||
style={{ width: `${((rate.employee_wage * rate.markup_percentage / 100) / rate.client_rate) * 100}%` }}
|
||
title={`Markup: ${rate.markup_percentage}%`}
|
||
>
|
||
{rate.markup_percentage}%
|
||
</div>
|
||
<div
|
||
className="bg-purple-500 flex items-center justify-center text-white"
|
||
style={{ width: `${((rate.employee_wage * (1 + rate.markup_percentage / 100) * vendorFee / 100) / rate.client_rate) * 100}%` }}
|
||
title={`VA Fee: ${vendorFee}%`}
|
||
>
|
||
{vendorFee}%
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="flex justify-between text-[10px] text-slate-600">
|
||
<span>Payrate: ${rate.employee_wage}/hr</span>
|
||
<span>Markup: {rate.markup_percentage}% (+${(rate.employee_wage * rate.markup_percentage / 100).toFixed(2)})</span>
|
||
<span>VA: {vendorFee}% (+${(rate.employee_wage * (1 + rate.markup_percentage / 100) * vendorFee / 100).toFixed(2)})</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* AI Insights */}
|
||
<div className="bg-slate-50 rounded-lg p-3">
|
||
<p className="text-xs font-semibold text-slate-700 mb-2">💡 AI Insights</p>
|
||
<div className="space-y-1 text-[10px]">
|
||
<div className="flex justify-between">
|
||
<span className="text-slate-600">Market Avg:</span>
|
||
<span className="font-semibold">${marketAverage.toFixed(2)}/hr</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-slate-600">Your Rate:</span>
|
||
<span className="font-semibold text-[#0A39DF]">${rate.client_rate}/hr</span>
|
||
</div>
|
||
<div className="flex justify-between">
|
||
<span className="text-slate-600">Difference:</span>
|
||
<span className={`font-semibold ${rate.client_rate < marketAverage ? 'text-green-600' : 'text-red-600'}`}>
|
||
{rate.client_rate < marketAverage ? '-' : '+'}${Math.abs(rate.client_rate - marketAverage).toFixed(2)}
|
||
</span>
|
||
</div>
|
||
<div className="pt-2 mt-2 border-t border-slate-200">
|
||
<span className="text-slate-600">Fill Rate:</span>
|
||
<span className={`ml-1 font-bold ${status === 'competitive' ? 'text-green-600' : status === 'market' ? 'text-yellow-600' : 'text-red-600'}`}>
|
||
{status === 'competitive' ? '95%' : status === 'market' ? '75%' : '45%'}/yr
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Status Message */}
|
||
{status === 'competitive' && (
|
||
<div className="mt-3 p-2 bg-green-50 border border-green-200 rounded text-xs text-green-900">
|
||
✓ <strong>Good Deal:</strong> Your rate is {((1 - rate.client_rate / marketAverage) * 100).toFixed(0)}% below market average.
|
||
Highly competitive with predicted 95% fill rate.
|
||
</div>
|
||
)}
|
||
{status === 'market' && (
|
||
<div className="mt-3 p-2 bg-blue-50 border border-blue-200 rounded text-xs text-blue-900">
|
||
~ <strong>At Market:</strong> Your rate aligns with market average.
|
||
Competitive with predicted 75% fill rate.
|
||
</div>
|
||
)}
|
||
{status === 'risk' && (
|
||
<div className="mt-3 p-2 bg-red-50 border border-red-200 rounded text-xs text-red-900 flex items-start gap-2">
|
||
<AlertCircle className="w-4 h-4 mt-0.5 flex-shrink-0" />
|
||
<div>
|
||
<strong>! Risk Zone:</strong> Your rate is {(((rate.client_rate / marketAverage) - 1) * 100).toFixed(0)}% above market average.
|
||
This may reduce competitiveness with predicted 45% fill rate. Consider lowering markup.
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
{/* Summary Stats */}
|
||
<div className="mt-6 grid grid-cols-4 gap-4">
|
||
<Card className="border-green-200 bg-green-50">
|
||
<CardContent className="p-4 text-center">
|
||
<p className="text-2xl font-bold text-green-700">
|
||
{formData.rate_proposals.filter(r => r.is_active && r.client_rate < (r.employee_wage * 1.35 * 1.05)).length}
|
||
</p>
|
||
<p className="text-xs text-green-600 font-medium mt-1">✓ Good Deals</p>
|
||
</CardContent>
|
||
</Card>
|
||
<Card className="border-blue-200 bg-blue-50">
|
||
<CardContent className="p-4 text-center">
|
||
<p className="text-2xl font-bold text-blue-700">
|
||
{formData.rate_proposals.filter(r => r.is_active && r.client_rate >= (r.employee_wage * 1.35 * 1.05) && r.client_rate <= (r.employee_wage * 1.35 * 1.15)).length}
|
||
</p>
|
||
<p className="text-xs text-blue-600 font-medium mt-1">~ At Market</p>
|
||
</CardContent>
|
||
</Card>
|
||
<Card className="border-red-200 bg-red-50">
|
||
<CardContent className="p-4 text-center">
|
||
<p className="text-2xl font-bold text-red-700">
|
||
{formData.rate_proposals.filter(r => r.is_active && r.client_rate > (r.employee_wage * 1.35 * 1.15)).length}
|
||
</p>
|
||
<p className="text-xs text-red-600 font-medium mt-1">! Risk Zone</p>
|
||
</CardContent>
|
||
</Card>
|
||
<Card className="border-slate-200 bg-slate-50">
|
||
<CardContent className="p-4 text-center">
|
||
<p className="text-2xl font-bold text-slate-700">
|
||
{formData.rate_proposals.filter(r => r.is_active).length}
|
||
</p>
|
||
<p className="text-xs text-slate-600 font-medium mt-1">Active Roles</p>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
|
||
{/* STEP 7: AI Intelligence (was Step 6) */}
|
||
{currentStep === 7 && (
|
||
<Card className="border-2 border-slate-200">
|
||
<CardHeader className="bg-gradient-to-r from-purple-50 to-pink-50 border-b">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-12 h-12 bg-gradient-to-r from-purple-600 to-pink-600 rounded-xl flex items-center justify-center">
|
||
<Sparkles className="w-6 h-6 text-white" />
|
||
</div>
|
||
<div>
|
||
<CardTitle className="text-2xl">AI Intelligence</CardTitle>
|
||
<p className="text-sm text-slate-600 mt-1">Market insights & recommendations</p>
|
||
</div>
|
||
</div>
|
||
{!aiInsights && (
|
||
<Button
|
||
onClick={generateAIInsights}
|
||
className="bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700"
|
||
>
|
||
<Sparkles className="w-4 h-4 mr-2" />
|
||
Generate Insights
|
||
</Button>
|
||
)}
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent className="p-6">
|
||
{!aiInsights ? (
|
||
<div className="text-center py-12">
|
||
<Sparkles className="w-16 h-16 mx-auto mb-3 text-purple-300" />
|
||
<p className="text-slate-600 mb-2">Ready to analyze your application</p>
|
||
<p className="text-sm text-slate-500">Get AI-powered insights on your competitive positioning</p>
|
||
</div>
|
||
) : (
|
||
<div className="space-y-6">
|
||
{/* Overall Score */}
|
||
<div className="p-6 bg-gradient-to-r from-purple-50 to-pink-50 rounded-lg border border-purple-200">
|
||
<div className="flex items-center justify-between mb-4">
|
||
<h3 className="font-semibold text-lg">Overall Assessment</h3>
|
||
<div className="text-4xl font-bold text-purple-600">
|
||
{aiInsights.overall_score}/100
|
||
</div>
|
||
</div>
|
||
<p className="text-sm text-slate-700">{aiInsights.competitive_position}</p>
|
||
</div>
|
||
|
||
{/* Strengths */}
|
||
{aiInsights.strengths && aiInsights.strengths.length > 0 && (
|
||
<div>
|
||
<h4 className="font-semibold mb-3 flex items-center gap-2">
|
||
<CheckCircle2 className="w-5 h-5 text-green-600" />
|
||
Strengths
|
||
</h4>
|
||
<div className="space-y-2">
|
||
{aiInsights.strengths.map((strength, idx) => (
|
||
<div key={idx} className="p-3 bg-green-50 border border-green-200 rounded-lg">
|
||
<p className="text-sm text-green-900">✓ {strength}</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Recommendations */}
|
||
{aiInsights.recommendations && aiInsights.recommendations.length > 0 && (
|
||
<div>
|
||
<h4 className="font-semibold mb-3 flex items-center gap-2">
|
||
<TrendingUp className="w-5 h-5 text-blue-600" />
|
||
Recommendations
|
||
</h4>
|
||
<div className="space-y-2">
|
||
{aiInsights.recommendations.map((rec, idx) => (
|
||
<div key={idx} className="p-3 bg-blue-50 border border-blue-200 rounded-lg">
|
||
<p className="text-sm text-blue-900">💡 {rec}</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Analysis Details */}
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div className="p-4 bg-slate-50 rounded-lg">
|
||
<h5 className="font-semibold text-sm mb-2">Market Alignment</h5>
|
||
<p className="text-xs text-slate-600">{aiInsights.market_alignment}</p>
|
||
</div>
|
||
<div className="p-4 bg-slate-50 rounded-lg">
|
||
<h5 className="font-semibold text-sm mb-2">Pricing Analysis</h5>
|
||
<p className="text-xs text-slate-600">{aiInsights.pricing_analysis}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
|
||
{/* Navigation Buttons */}
|
||
{currentStep > 0 && (
|
||
<div className="flex items-center justify-between mt-6">
|
||
<Button
|
||
variant="outline"
|
||
onClick={handleBack}
|
||
disabled={currentStep === 1}
|
||
className="border-slate-300 hover:bg-slate-50"
|
||
>
|
||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||
Back
|
||
</Button>
|
||
|
||
{currentStep < steps.length - 1 ? (
|
||
<Button
|
||
onClick={handleNext}
|
||
className="bg-[#0A39DF] hover:bg-[#0A39DF]/90 text-white"
|
||
>
|
||
Next Step
|
||
<ArrowRight className="w-4 h-4 mr-2" />
|
||
</Button>
|
||
) : (
|
||
<Button
|
||
onClick={handleSubmit}
|
||
disabled={submitApplicationMutation.isPending}
|
||
className="bg-gradient-to-r from-green-600 to-emerald-600 hover:from-green-700 hover:to-emerald-700"
|
||
>
|
||
{submitApplicationMutation.isPending ? (
|
||
<>
|
||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||
Submitting...
|
||
</>
|
||
) : (
|
||
<>
|
||
<CheckCircle2 className="w-4 h-4 mr-2" />
|
||
Submit Application
|
||
</>
|
||
)}
|
||
</Button>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|