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:
bwnyasse
2025-11-12 12:50:55 -05:00
parent 92fd0118be
commit 554dc9f9e3
203 changed files with 1414 additions and 732 deletions

View File

@@ -0,0 +1,513 @@
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>
);
}