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

feat: add vendor marketplace page
feat: add global import fix to prepare-export script
feat: add patch-layout-query-key script to fix query key
feat: update patch-base44-client script to use a more robust method
2025-11-13 14:56:31 -05:00

381 lines
16 KiB
JavaScript

import React, { useState } from "react";
import { base44 } from "@/api/base44Client";
import { useMutation, useQueryClient, 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 {
Send, ArrowLeft, Mail, Building2, User, Percent,
CheckCircle2, Loader2, Info, Sparkles
} from "lucide-react";
import { useToast } from "@/components/ui/use-toast";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
import PageHeader from "@/components/common/PageHeader";
export default function InviteVendor() {
const { toast } = useToast();
const queryClient = useQueryClient();
const navigate = useNavigate();
const { data: user } = useQuery({
queryKey: ['current-user-invite'],
queryFn: () => base44.auth.me(),
});
const [inviteData, setInviteData] = useState({
company_name: "",
primary_contact_name: "",
primary_contact_email: "",
vendor_admin_fee: 12,
notes: ""
});
const sendInviteMutation = useMutation({
mutationFn: async (data) => {
// Generate unique invite code
const inviteCode = `INV-${Math.floor(10000 + Math.random() * 90000)}`;
// Create invite record
const invite = await base44.entities.VendorInvite.create({
invite_code: inviteCode,
company_name: data.company_name,
primary_contact_name: data.primary_contact_name,
primary_contact_email: data.primary_contact_email,
vendor_admin_fee: parseFloat(data.vendor_admin_fee),
invited_by: user?.email || "admin",
invite_status: "pending",
invite_sent_date: new Date().toISOString(),
notes: data.notes
});
// Send email to vendor
const onboardingUrl = `${window.location.origin}${createPageUrl('SmartVendorOnboarding')}?invite=${inviteCode}`;
await base44.integrations.Core.SendEmail({
from_name: "KROW Platform",
to: data.primary_contact_email,
subject: `Welcome to KROW! You're Invited to Join Our Vendor Network`,
body: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; background: linear-gradient(to bottom, #f8fafc, #e0f2fe); border-radius: 12px;">
<div style="text-align: center; padding: 20px; background: linear-gradient(to right, #0A39DF, #1C323E); border-radius: 12px 12px 0 0;">
<h1 style="color: white; margin: 0;">Welcome to KROW!</h1>
<p style="color: #e0f2fe; margin-top: 8px;">You've been invited to join our vendor network</p>
</div>
<div style="padding: 30px; background: white; border-radius: 0 0 12px 12px;">
<p style="color: #1C323E; font-size: 16px; line-height: 1.6;">
Hi ${data.primary_contact_name},
</p>
<p style="color: #475569; font-size: 14px; line-height: 1.6; margin-top: 16px;">
Great news! We'd like to invite <strong>${data.company_name}</strong> to join the KROW vendor network.
Our platform connects top-tier service providers with clients who need reliable, high-quality staffing solutions.
</p>
<div style="background: #f1f5f9; padding: 20px; border-radius: 8px; margin: 24px 0; border-left: 4px solid #0A39DF;">
<h3 style="color: #1C323E; margin: 0 0 12px 0; font-size: 16px;">Why Join KROW?</h3>
<ul style="color: #475569; margin: 0; padding-left: 20px; line-height: 1.8;">
<li>Access to vetted enterprise clients</li>
<li>Streamlined order management</li>
<li>Instant AI-powered rate proposals</li>
<li>Real-time market intelligence</li>
<li>Automated compliance tracking</li>
</ul>
</div>
<div style="text-align: center; margin: 32px 0;">
<a href="${onboardingUrl}"
style="display: inline-block; padding: 16px 32px; background: linear-gradient(to right, #0A39DF, #1C323E);
color: white; text-decoration: none; border-radius: 8px; font-weight: bold; font-size: 16px;
box-shadow: 0 4px 6px rgba(10, 57, 223, 0.3);">
Start Onboarding →
</a>
</div>
<div style="background: #fef3c7; padding: 16px; border-radius: 8px; margin-top: 24px; border: 1px solid #fbbf24;">
<p style="color: #92400e; font-size: 13px; margin: 0; line-height: 1.6;">
<strong>📋 What to Prepare:</strong><br>
• W-9 Tax Form<br>
• Certificate of Insurance (COI)<br>
• Secretary of State Certificate<br>
• List of positions you can provide<br>
• Your competitive rate proposals
</p>
</div>
<p style="color: #64748b; font-size: 13px; margin-top: 32px; padding-top: 20px; border-top: 1px solid #e2e8f0;">
Your invite code: <strong>${inviteCode}</strong><br>
Vendor fee: <strong>${data.vendor_admin_fee}%</strong><br>
Questions? Reply to this email or contact us at support@krow.com
</p>
<p style="color: #94a3b8; font-size: 12px; margin-top: 16px;">
This invitation was sent by ${user?.full_name || user?.email} from KROW.
</p>
</div>
</div>
`
});
return invite;
},
onSuccess: (invite) => {
queryClient.invalidateQueries(['vendor-invites']);
toast({
title: "Invite Sent Successfully!",
description: `${inviteData.company_name} will receive an email at ${inviteData.primary_contact_email}`,
});
// Reset form
setInviteData({
company_name: "",
primary_contact_name: "",
primary_contact_email: "",
vendor_admin_fee: 12,
notes: ""
});
},
onError: (error) => {
toast({
title: "Failed to Send Invite",
description: error.message,
variant: "destructive"
});
}
});
const handleSubmit = (e) => {
e.preventDefault();
if (!inviteData.company_name || !inviteData.primary_contact_email) {
toast({
title: "Missing Information",
description: "Please fill in company name and contact email",
variant: "destructive"
});
return;
}
sendInviteMutation.mutate(inviteData);
};
return (
<div className="p-4 md:p-8 bg-gradient-to-br from-slate-50 to-blue-50/30 min-h-screen">
<div className="max-w-3xl mx-auto">
<PageHeader
title="Invite Vendor to KROW"
subtitle="Send a personalized onboarding invitation"
actions={
<Button
variant="outline"
onClick={() => navigate(createPageUrl("VendorManagement"))}
>
<ArrowLeft className="w-4 h-4 mr-2" />
Back to Vendors
</Button>
}
/>
{/* Info Banner */}
<Card className="mb-6 border-blue-200 bg-blue-50">
<CardContent className="p-4">
<div className="flex items-start gap-3">
<Sparkles className="w-5 h-5 text-[#0A39DF] mt-0.5" />
<div>
<h4 className="font-semibold text-[#1C323E] mb-1">Smart Vendor Onboarding</h4>
<p className="text-sm text-slate-600">
Send an invite with a custom vendor fee. The vendor will receive a welcome email
with an onboarding link powered by AI for document validation, rate proposals, and market intelligence.
</p>
</div>
</div>
</CardContent>
</Card>
{/* Invite Form */}
<form onSubmit={handleSubmit}>
<Card className="border-2 border-slate-200 shadow-lg">
<CardHeader className="bg-gradient-to-r from-slate-50 to-blue-50 border-b">
<div className="flex items-center gap-3">
<Send className="w-6 h-6 text-[#0A39DF]" />
<div>
<CardTitle className="text-2xl text-[#1C323E]">Vendor Invitation Details</CardTitle>
<p className="text-sm text-slate-600 mt-1">Fill in the vendor information below</p>
</div>
</div>
</CardHeader>
<CardContent className="p-6 space-y-6">
{/* Company Name */}
<div>
<Label htmlFor="company_name" className="flex items-center gap-2">
<Building2 className="w-4 h-4 text-[#0A39DF]" />
Company Name *
</Label>
<Input
id="company_name"
value={inviteData.company_name}
onChange={(e) => setInviteData(prev => ({ ...prev, company_name: e.target.value }))}
placeholder="ABC Staffing Solutions LLC"
className="mt-2"
required
/>
</div>
{/* Contact Information */}
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="contact_name" className="flex items-center gap-2">
<User className="w-4 h-4 text-[#0A39DF]" />
Primary Contact Name
</Label>
<Input
id="contact_name"
value={inviteData.primary_contact_name}
onChange={(e) => setInviteData(prev => ({ ...prev, primary_contact_name: e.target.value }))}
placeholder="John Smith"
className="mt-2"
/>
</div>
<div>
<Label htmlFor="contact_email" className="flex items-center gap-2">
<Mail className="w-4 h-4 text-[#0A39DF]" />
Contact Email *
</Label>
<Input
id="contact_email"
type="email"
value={inviteData.primary_contact_email}
onChange={(e) => setInviteData(prev => ({ ...prev, primary_contact_email: e.target.value }))}
placeholder="john@abcstaffing.com"
className="mt-2"
required
/>
</div>
</div>
{/* Vendor Fee */}
<div>
<Label htmlFor="vendor_admin_fee" className="flex items-center gap-2">
<Percent className="w-4 h-4 text-[#0A39DF]" />
Vendor Fee (%) *
<HoverCard>
<HoverCardTrigger>
<Info className="w-4 h-4 text-slate-400 cursor-help" />
</HoverCardTrigger>
<HoverCardContent className="w-80">
<div className="space-y-2">
<h4 className="font-semibold text-[#1C323E]">What is Vendor Fee?</h4>
<p className="text-sm text-slate-600">
The vendor fee is a percentage charged on top of the vendor's rate to cover
platform services, compliance, insurance, and payment processing.
</p>
<p className="text-sm text-slate-600">
Standard: <strong>12%</strong> • Can be adjusted based on vendor tier,
volume commitments, or special partnerships.
</p>
</div>
</HoverCardContent>
</HoverCard>
</Label>
<div className="flex items-center gap-3 mt-2">
<Input
id="vendor_admin_fee"
type="number"
step="0.1"
min="0"
max="100"
value={inviteData.vendor_admin_fee}
onChange={(e) => setInviteData(prev => ({ ...prev, vendor_admin_fee: e.target.value }))}
className="w-32"
required
/>
<span className="text-sm text-slate-600">
This fee will be automatically applied to all vendor rate proposals
</span>
</div>
</div>
{/* Internal Notes */}
<div>
<Label htmlFor="notes">Internal Notes (Optional)</Label>
<Textarea
id="notes"
value={inviteData.notes}
onChange={(e) => setInviteData(prev => ({ ...prev, notes: e.target.value }))}
placeholder="Add any internal notes about this vendor..."
className="mt-2"
rows={3}
/>
<p className="text-xs text-slate-500 mt-1">These notes are for internal use only and won't be shared with the vendor</p>
</div>
{/* Preview Card */}
<Card className="bg-gradient-to-br from-blue-50 to-white border-[#0A39DF]/30">
<CardContent className="p-4">
<h4 className="font-semibold text-[#1C323E] mb-3">Invitation Preview</h4>
<div className="space-y-2 text-sm">
<div className="flex items-center justify-between">
<span className="text-slate-600">Company:</span>
<span className="font-medium text-[#1C323E]">{inviteData.company_name || "—"}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-slate-600">Contact:</span>
<span className="font-medium text-[#1C323E]">{inviteData.primary_contact_name || "—"}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-slate-600">Email:</span>
<span className="font-medium text-[#1C323E]">{inviteData.primary_contact_email || "—"}</span>
</div>
<div className="flex items-center justify-between pt-2 border-t">
<span className="text-slate-600">Vendor Fee:</span>
<Badge className="bg-[#0A39DF] text-white">{inviteData.vendor_admin_fee}%</Badge>
</div>
</div>
</CardContent>
</Card>
</CardContent>
</Card>
{/* Action Buttons */}
<div className="flex items-center justify-between mt-6">
<Button
type="button"
variant="outline"
onClick={() => navigate(createPageUrl("VendorManagement"))}
>
Cancel
</Button>
<Button
type="submit"
disabled={sendInviteMutation.isPending}
className="bg-gradient-to-r from-[#0A39DF] to-[#1C323E] hover:from-[#0A39DF]/90 hover:to-[#1C323E]/90"
>
{sendInviteMutation.isPending ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Sending Invite...
</>
) : (
<>
<Send className="w-4 h-4 mr-2" />
Send Invitation
</>
)}
</Button>
</div>
</form>
</div>
</div>
);
}