Files
Krow-workspace/frontend-web/src/pages/Onboarding.jsx
bwnyasse 554dc9f9e3 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.
2025-11-12 12:50:55 -05:00

513 lines
20 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}