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
381 lines
16 KiB
JavaScript
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>
|
|
);
|
|
}
|