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.
513 lines
20 KiB
JavaScript
513 lines
20 KiB
JavaScript
import React, { useState } from "react";
|
||
import { base44 } from "@/api/base44Client";
|
||
import { useQuery, useMutation } 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 { CheckCircle2, UserPlus, User, Lock, Briefcase } from "lucide-react";
|
||
import { useToast } from "@/components/ui/use-toast";
|
||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||
|
||
export default function Onboarding() {
|
||
const navigate = useNavigate();
|
||
const { toast } = useToast();
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const inviteCode = urlParams.get('invite');
|
||
|
||
const [step, setStep] = useState(1);
|
||
const [formData, setFormData] = useState({
|
||
first_name: "",
|
||
last_name: "",
|
||
email: "",
|
||
phone: "",
|
||
title: "",
|
||
department: "",
|
||
hub: "",
|
||
password: "",
|
||
confirmPassword: ""
|
||
});
|
||
|
||
// Fetch invite details if invite code exists
|
||
const { data: invite } = useQuery({
|
||
queryKey: ['team-invite', inviteCode],
|
||
queryFn: async () => {
|
||
const allInvites = await base44.entities.TeamMemberInvite.list();
|
||
const foundInvite = allInvites.find(inv => inv.invite_code === inviteCode && inv.invite_status === 'pending');
|
||
|
||
if (foundInvite) {
|
||
// Pre-fill form with invite data
|
||
const nameParts = (foundInvite.full_name || "").split(' ');
|
||
setFormData(prev => ({
|
||
...prev,
|
||
email: foundInvite.email,
|
||
first_name: nameParts[0] || "",
|
||
last_name: nameParts.slice(1).join(' ') || ""
|
||
}));
|
||
}
|
||
|
||
return foundInvite;
|
||
},
|
||
enabled: !!inviteCode,
|
||
});
|
||
|
||
// Fetch available hubs for the team
|
||
const { data: hubs = [] } = useQuery({
|
||
queryKey: ['team-hubs-onboarding', invite?.team_id],
|
||
queryFn: async () => {
|
||
if (!invite?.team_id) return [];
|
||
const allHubs = await base44.entities.TeamHub.list();
|
||
return allHubs.filter(h => h.team_id === invite.team_id && h.is_active);
|
||
},
|
||
enabled: !!invite?.team_id,
|
||
initialData: [],
|
||
});
|
||
|
||
const registerMutation = useMutation({
|
||
mutationFn: async (data) => {
|
||
if (!invite) {
|
||
throw new Error("Invalid invitation. Please contact your team administrator.");
|
||
}
|
||
|
||
// Check if invite was already accepted
|
||
if (invite.invite_status !== 'pending') {
|
||
throw new Error("This invitation has already been used. Please contact your team administrator for a new invitation.");
|
||
}
|
||
|
||
// Check for duplicate email in TeamMember
|
||
const allMembers = await base44.entities.TeamMember.list();
|
||
const existingMemberByEmail = allMembers.find(m =>
|
||
m.email?.toLowerCase() === data.email.toLowerCase() && m.team_id === invite.team_id
|
||
);
|
||
|
||
if (existingMemberByEmail) {
|
||
throw new Error(`A team member with email ${data.email} already exists in this team. Please contact your team administrator.`);
|
||
}
|
||
|
||
// Check for duplicate phone in TeamMember
|
||
if (data.phone) {
|
||
const existingMemberByPhone = allMembers.find(m =>
|
||
m.phone === data.phone && m.team_id === invite.team_id
|
||
);
|
||
|
||
if (existingMemberByPhone) {
|
||
throw new Error(`A team member with phone number ${data.phone} already exists in this team. Please contact your team administrator.`);
|
||
}
|
||
}
|
||
|
||
// Create team member record
|
||
const member = await base44.entities.TeamMember.create({
|
||
team_id: invite.team_id,
|
||
member_name: `${data.first_name} ${data.last_name}`.trim(),
|
||
email: data.email,
|
||
phone: data.phone,
|
||
title: data.title,
|
||
department: data.department,
|
||
hub: data.hub,
|
||
role: invite.role || "member",
|
||
is_active: true,
|
||
});
|
||
|
||
// Update invite status to accepted
|
||
await base44.entities.TeamMemberInvite.update(invite.id, {
|
||
invite_status: "accepted",
|
||
accepted_date: new Date().toISOString()
|
||
});
|
||
|
||
return { member, invite };
|
||
},
|
||
onSuccess: () => {
|
||
setStep(4);
|
||
toast({
|
||
title: "✅ Registration Successful!",
|
||
description: "You've been added to the team successfully.",
|
||
});
|
||
},
|
||
onError: (error) => {
|
||
toast({
|
||
title: "❌ Registration Failed",
|
||
description: error.message,
|
||
variant: "destructive",
|
||
});
|
||
},
|
||
});
|
||
|
||
const handleNext = () => {
|
||
if (step === 1) {
|
||
// Validate basic info
|
||
if (!formData.first_name || !formData.last_name || !formData.email || !formData.phone) {
|
||
toast({
|
||
title: "Missing Information",
|
||
description: "Please fill in your name, email, and phone number",
|
||
variant: "destructive",
|
||
});
|
||
return;
|
||
}
|
||
setStep(2);
|
||
} else if (step === 2) {
|
||
// Validate additional info
|
||
if (!formData.title || !formData.department) {
|
||
toast({
|
||
title: "Missing Information",
|
||
description: "Please fill in your title and department",
|
||
variant: "destructive",
|
||
});
|
||
return;
|
||
}
|
||
setStep(3);
|
||
} else if (step === 3) {
|
||
// Validate password
|
||
if (!formData.password || formData.password !== formData.confirmPassword) {
|
||
toast({
|
||
title: "Password Mismatch",
|
||
description: "Passwords do not match",
|
||
variant: "destructive",
|
||
});
|
||
return;
|
||
}
|
||
if (formData.password.length < 6) {
|
||
toast({
|
||
title: "Password Too Short",
|
||
description: "Password must be at least 6 characters",
|
||
variant: "destructive",
|
||
});
|
||
return;
|
||
}
|
||
registerMutation.mutate(formData);
|
||
}
|
||
};
|
||
|
||
if (!inviteCode || !invite) {
|
||
return (
|
||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-50 flex items-center justify-center p-4">
|
||
<Card className="max-w-md w-full border-2 border-red-200">
|
||
<CardContent className="p-12 text-center">
|
||
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-6">
|
||
<span className="text-4xl">❌</span>
|
||
</div>
|
||
<h2 className="text-2xl font-bold text-[#1C323E] mb-4">Invalid Invitation</h2>
|
||
<p className="text-slate-600 mb-6">
|
||
This invitation link is invalid or has expired. Please contact your team administrator for a new invitation.
|
||
</p>
|
||
<Button onClick={() => navigate(createPageUrl("Home"))} variant="outline">
|
||
Go to Home
|
||
</Button>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// Check if invite was already accepted
|
||
if (invite.invite_status === 'accepted') {
|
||
return (
|
||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-50 flex items-center justify-center p-4">
|
||
<Card className="max-w-md w-full border-2 border-yellow-200">
|
||
<CardContent className="p-12 text-center">
|
||
<div className="w-16 h-16 bg-yellow-100 rounded-full flex items-center justify-center mx-auto mb-6">
|
||
<span className="text-4xl">⚠️</span>
|
||
</div>
|
||
<h2 className="text-2xl font-bold text-[#1C323E] mb-4">Invitation Already Used</h2>
|
||
<p className="text-slate-600 mb-6">
|
||
This invitation has already been accepted. If you need access, please contact your team administrator.
|
||
</p>
|
||
<Button onClick={() => navigate(createPageUrl("Home"))} variant="outline">
|
||
Go to Home
|
||
</Button>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-50 flex items-center justify-center p-4">
|
||
<div className="max-w-2xl w-full">
|
||
{/* Header */}
|
||
<div className="text-center mb-8">
|
||
<div className="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-br from-[#0A39DF] to-[#1C323E] rounded-full mb-4">
|
||
<UserPlus className="w-8 h-8 text-white" />
|
||
</div>
|
||
<h1 className="text-4xl font-bold bg-gradient-to-r from-[#1C323E] to-[#0A39DF] bg-clip-text text-transparent mb-2">
|
||
Join {invite.team_name}
|
||
</h1>
|
||
<p className="text-slate-600">
|
||
You've been invited by {invite.invited_by} as a <strong>{invite.role}</strong>
|
||
</p>
|
||
</div>
|
||
|
||
{/* Progress Steps */}
|
||
{step < 4 && (
|
||
<div className="flex items-center justify-center mb-8">
|
||
<div className="flex items-center gap-2">
|
||
<div className={`flex items-center justify-center w-10 h-10 rounded-full ${step >= 1 ? 'bg-[#0A39DF] text-white' : 'bg-slate-200 text-slate-500'}`}>
|
||
{step > 1 ? <CheckCircle2 className="w-5 h-5" /> : '1'}
|
||
</div>
|
||
<div className={`w-20 h-1 ${step >= 2 ? 'bg-[#0A39DF]' : 'bg-slate-200'}`} />
|
||
<div className={`flex items-center justify-center w-10 h-10 rounded-full ${step >= 2 ? 'bg-[#0A39DF] text-white' : 'bg-slate-200 text-slate-500'}`}>
|
||
{step > 2 ? <CheckCircle2 className="w-5 h-5" /> : '2'}
|
||
</div>
|
||
<div className={`w-20 h-1 ${step >= 3 ? 'bg-[#0A39DF]' : 'bg-slate-200'}`} />
|
||
<div className={`flex items-center justify-center w-10 h-10 rounded-full ${step >= 3 ? 'bg-[#0A39DF] text-white' : 'bg-slate-200 text-slate-500'}`}>
|
||
{step > 3 ? <CheckCircle2 className="w-5 h-5" /> : '3'}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Step 1: Basic Information */}
|
||
{step === 1 && (
|
||
<Card className="border-2 border-slate-200 shadow-xl">
|
||
<CardHeader className="bg-gradient-to-r from-slate-50 to-blue-50">
|
||
<CardTitle className="flex items-center gap-2">
|
||
<User className="w-5 h-5 text-[#0A39DF]" />
|
||
Basic Information
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="p-6 space-y-4">
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<Label htmlFor="first_name">First Name *</Label>
|
||
<Input
|
||
id="first_name"
|
||
value={formData.first_name}
|
||
onChange={(e) => setFormData({ ...formData, first_name: e.target.value })}
|
||
placeholder="John"
|
||
className="mt-2"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<Label htmlFor="last_name">Last Name *</Label>
|
||
<Input
|
||
id="last_name"
|
||
value={formData.last_name}
|
||
onChange={(e) => setFormData({ ...formData, last_name: e.target.value })}
|
||
placeholder="Doe"
|
||
className="mt-2"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<Label htmlFor="email">Email *</Label>
|
||
<Input
|
||
id="email"
|
||
type="email"
|
||
value={formData.email}
|
||
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||
placeholder="john@example.com"
|
||
className="mt-2"
|
||
disabled={!!invite}
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<Label htmlFor="phone">Phone Number *</Label>
|
||
<Input
|
||
id="phone"
|
||
type="tel"
|
||
value={formData.phone}
|
||
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
|
||
placeholder="+1 (555) 123-4567"
|
||
className="mt-2"
|
||
/>
|
||
</div>
|
||
|
||
<Button
|
||
onClick={handleNext}
|
||
className="w-full bg-gradient-to-r from-[#0A39DF] to-[#1C323E] hover:opacity-90"
|
||
>
|
||
Continue
|
||
</Button>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
|
||
{/* Step 2: Work Information */}
|
||
{step === 2 && (
|
||
<Card className="border-2 border-slate-200 shadow-xl">
|
||
<CardHeader className="bg-gradient-to-r from-slate-50 to-blue-50">
|
||
<CardTitle className="flex items-center gap-2">
|
||
<Briefcase className="w-5 h-5 text-[#0A39DF]" />
|
||
Work Information
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="p-6 space-y-4">
|
||
<div>
|
||
<Label htmlFor="title">Job Title *</Label>
|
||
<Input
|
||
id="title"
|
||
value={formData.title}
|
||
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
|
||
placeholder="e.g., Manager, Coordinator, Supervisor"
|
||
className="mt-2"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<Label htmlFor="department">Department *</Label>
|
||
<Select value={formData.department} onValueChange={(value) => setFormData({ ...formData, department: value })}>
|
||
<SelectTrigger className="mt-2">
|
||
<SelectValue placeholder="Select department" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="Operations">Operations</SelectItem>
|
||
<SelectItem value="Sales">Sales</SelectItem>
|
||
<SelectItem value="HR">HR</SelectItem>
|
||
<SelectItem value="Finance">Finance</SelectItem>
|
||
<SelectItem value="IT">IT</SelectItem>
|
||
<SelectItem value="Marketing">Marketing</SelectItem>
|
||
<SelectItem value="Customer Service">Customer Service</SelectItem>
|
||
<SelectItem value="Logistics">Logistics</SelectItem>
|
||
<SelectItem value="Management">Management</SelectItem>
|
||
<SelectItem value="Other">Other</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
|
||
{hubs.length > 0 && (
|
||
<div>
|
||
<Label htmlFor="hub">Hub Location (Optional)</Label>
|
||
<Select value={formData.hub} onValueChange={(value) => setFormData({ ...formData, hub: value })}>
|
||
<SelectTrigger className="mt-2">
|
||
<SelectValue placeholder="Select hub location" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value={null}>No Hub</SelectItem>
|
||
{hubs.map((hub) => (
|
||
<SelectItem key={hub.id} value={hub.hub_name}>
|
||
{hub.hub_name}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
)}
|
||
|
||
<div className="flex gap-3">
|
||
<Button
|
||
variant="outline"
|
||
onClick={() => setStep(1)}
|
||
className="flex-1"
|
||
>
|
||
Back
|
||
</Button>
|
||
<Button
|
||
onClick={handleNext}
|
||
className="flex-1 bg-gradient-to-r from-[#0A39DF] to-[#1C323E] hover:opacity-90"
|
||
>
|
||
Continue
|
||
</Button>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
|
||
{/* Step 3: Create Password */}
|
||
{step === 3 && (
|
||
<Card className="border-2 border-slate-200 shadow-xl">
|
||
<CardHeader className="bg-gradient-to-r from-slate-50 to-blue-50">
|
||
<CardTitle className="flex items-center gap-2">
|
||
<Lock className="w-5 h-5 text-[#0A39DF]" />
|
||
Create Your Password
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="p-6 space-y-4">
|
||
<div>
|
||
<Label htmlFor="password">Password *</Label>
|
||
<Input
|
||
id="password"
|
||
type="password"
|
||
value={formData.password}
|
||
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
||
placeholder="••••••••"
|
||
className="mt-2"
|
||
/>
|
||
<p className="text-xs text-slate-500 mt-1">Minimum 6 characters</p>
|
||
</div>
|
||
|
||
<div>
|
||
<Label htmlFor="confirmPassword">Confirm Password *</Label>
|
||
<Input
|
||
id="confirmPassword"
|
||
type="password"
|
||
value={formData.confirmPassword}
|
||
onChange={(e) => setFormData({ ...formData, confirmPassword: e.target.value })}
|
||
placeholder="••••••••"
|
||
className="mt-2"
|
||
/>
|
||
</div>
|
||
|
||
<div className="bg-blue-50 p-4 rounded-lg border border-blue-200">
|
||
<h4 className="font-semibold text-[#1C323E] mb-2">Review Your Information:</h4>
|
||
<div className="space-y-1 text-sm text-slate-600">
|
||
<p><strong>Name:</strong> {formData.first_name} {formData.last_name}</p>
|
||
<p><strong>Email:</strong> {formData.email}</p>
|
||
<p><strong>Phone:</strong> {formData.phone}</p>
|
||
<p><strong>Title:</strong> {formData.title}</p>
|
||
<p><strong>Department:</strong> {formData.department}</p>
|
||
{formData.hub && <p><strong>Hub:</strong> {formData.hub}</p>}
|
||
<p><strong>Role:</strong> {invite.role}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex gap-3">
|
||
<Button
|
||
variant="outline"
|
||
onClick={() => setStep(2)}
|
||
className="flex-1"
|
||
disabled={registerMutation.isPending}
|
||
>
|
||
Back
|
||
</Button>
|
||
<Button
|
||
onClick={handleNext}
|
||
disabled={registerMutation.isPending}
|
||
className="flex-1 bg-gradient-to-r from-[#0A39DF] to-[#1C323E] hover:opacity-90"
|
||
>
|
||
{registerMutation.isPending ? 'Creating Account...' : 'Complete Registration'}
|
||
</Button>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
|
||
{/* Step 4: Success */}
|
||
{step === 4 && (
|
||
<Card className="border-2 border-green-200 shadow-xl">
|
||
<CardContent className="p-12 text-center">
|
||
<div className="inline-flex items-center justify-center w-20 h-20 bg-green-100 rounded-full mb-6">
|
||
<CheckCircle2 className="w-12 h-12 text-green-600" />
|
||
</div>
|
||
<h2 className="text-3xl font-bold text-[#1C323E] mb-4">
|
||
Welcome to the Team! 🎉
|
||
</h2>
|
||
<p className="text-slate-600 mb-2">
|
||
Your account has been created successfully!
|
||
</p>
|
||
<div className="bg-slate-50 p-4 rounded-lg mb-8 text-left">
|
||
<h3 className="font-semibold text-[#1C323E] mb-2">Your Profile:</h3>
|
||
<div className="space-y-1 text-sm text-slate-600">
|
||
<p><strong>Name:</strong> {formData.first_name} {formData.last_name}</p>
|
||
<p><strong>Email:</strong> {formData.email}</p>
|
||
<p><strong>Title:</strong> {formData.title}</p>
|
||
<p><strong>Department:</strong> {formData.department}</p>
|
||
{formData.hub && <p><strong>Hub:</strong> {formData.hub}</p>}
|
||
<p><strong>Team:</strong> {invite.team_name}</p>
|
||
</div>
|
||
</div>
|
||
<Button
|
||
onClick={() => navigate(createPageUrl("Dashboard"))}
|
||
className="bg-gradient-to-r from-[#0A39DF] to-[#1C323E] hover:opacity-90"
|
||
>
|
||
Go to Dashboard
|
||
</Button>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
} |