feat: Initialize monorepo structure and comprehensive documentation
This commit establishes the new monorepo architecture for the KROW Workforce platform. Key changes include: - Reorganized project into `frontend-web`, `mobile-apps`, `firebase`, `scripts`, and `secrets` directories. - Updated `Makefile` to support the new monorepo layout and automate Base44 export integration. - Fixed `scripts/prepare-export.js` for ES module compatibility and global component import resolution. - Created and updated `CONTRIBUTING.md` for developer onboarding. - Restructured, renamed, and translated all `docs/` files for clarity and consistency. - Implemented an interactive internal launchpad with diagram viewing capabilities. - Configured base Firebase project files (`firebase.json`, security rules). - Updated `README.md` to reflect the new project structure and documentation overview.
This commit is contained in:
380
frontend-web/src/pages/InviteVendor.jsx
Normal file
380
frontend-web/src/pages/InviteVendor.jsx
Normal file
@@ -0,0 +1,380 @@
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user