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,375 @@
import React, { useState } from "react";
import { base44 } from "@/api/base44Client";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
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 { Badge } from "@/components/ui/badge";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import { Users, UserPlus, Mail, Shield, Building2, Edit, Trash2 } from "lucide-react";
import { useToast } from "@/components/ui/use-toast";
import UserPermissionsModal from "../components/permissions/UserPermissionsModal"; // Import the new modal component
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; // Import Avatar components
export default function UserManagement() {
const [showInviteDialog, setShowInviteDialog] = useState(false);
const [inviteData, setInviteData] = useState({
email: "",
full_name: "",
user_role: "workforce",
company_name: "",
phone: "",
department: ""
});
const [selectedUser, setSelectedUser] = useState(null);
const [showPermissionsModal, setShowPermissionsModal] = useState(false);
const queryClient = useQueryClient();
const { toast } = useToast();
const { data: users } = useQuery({
queryKey: ['all-users'],
queryFn: async () => {
// Only admins can see all users
const allUsers = await base44.entities.User.list('-created_date');
return allUsers;
},
initialData: [],
});
const { data: currentUser } = useQuery({
queryKey: ['current-user'],
queryFn: () => base44.auth.me(),
});
const updateUserMutation = useMutation({
mutationFn: ({ userId, data }) => base44.entities.User.update(userId, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['all-users'] });
toast({
title: "User Updated",
description: "User role and information updated successfully",
});
setShowPermissionsModal(false); // Close the modal on success
setSelectedUser(null); // Clear selected user
},
onError: (error) => {
toast({
title: "Error updating user",
description: error.message || "Failed to update user information.",
variant: "destructive",
});
}
});
const handleInviteUser = async () => {
if (!inviteData.email || !inviteData.full_name) {
toast({
title: "Missing Information",
description: "Please fill in email and full name",
variant: "destructive"
});
return;
}
// In a real system, you would send an invitation email
// For now, we'll just create a user record
toast({
title: "User Invited",
description: `Invitation sent to ${inviteData.email}. They will receive setup instructions via email.`,
});
setShowInviteDialog(false);
setInviteData({
email: "",
full_name: "",
user_role: "workforce",
company_name: "",
phone: "",
department: ""
});
};
const getRoleColor = (role) => {
const colors = {
admin: "bg-red-100 text-red-700",
procurement: "bg-purple-100 text-purple-700",
operator: "bg-blue-100 text-blue-700",
sector: "bg-cyan-100 text-cyan-700",
client: "bg-green-100 text-green-700",
vendor: "bg-amber-100 text-amber-700",
workforce: "bg-slate-100 text-slate-700",
};
return colors[role] || "bg-slate-100 text-slate-700";
};
const getRoleLabel = (role) => {
const labels = {
admin: "Administrator",
procurement: "Procurement",
operator: "Operator",
sector: "Sector Manager",
client: "Client",
vendor: "Vendor",
workforce: "Workforce"
};
return labels[role] || role;
};
const handleEditPermissions = (user) => {
setSelectedUser(user);
setShowPermissionsModal(true);
};
const handleSavePermissions = async (updatedUser) => {
try {
// Assuming updatedUser contains the ID and the fields to update
// The updateUserMutation already handles base44.entities.User.update and success/error toasts
await updateUserMutation.mutateAsync({ userId: updatedUser.id, data: updatedUser });
} catch (error) {
// Error handling is already in updateUserMutation's onError callback
// No need to duplicate toast here unless specific error handling is required for this modal
}
};
// Only admins can access this page
if (currentUser?.user_role !== "admin" && currentUser?.role !== "admin") {
return (
<div className="p-8 text-center">
<Shield className="w-16 h-16 mx-auto text-red-500 mb-4" />
<h2 className="text-2xl font-bold text-slate-900 mb-2">Access Denied</h2>
<p className="text-slate-600">Only administrators can access user management.</p>
</div>
);
}
// Sample avatar for users without profile pictures
const sampleAvatar = "https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=400&h=400&fit=crop";
return (
<div className="p-4 md:p-8 bg-slate-50 min-h-screen">
<div className="max-w-7xl mx-auto">
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-3xl font-bold text-[#1C323E]">User Management</h1>
<p className="text-slate-500 mt-1">Manage users and assign roles</p>
</div>
<Button
onClick={() => setShowInviteDialog(true)}
className="bg-[#0A39DF] hover:bg-[#0A39DF]/90"
>
<UserPlus className="w-4 h-4 mr-2" />
Invite User
</Button>
</div>
{/* Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<Card className="border-slate-200">
<CardContent className="p-6">
<Users className="w-8 h-8 text-[#0A39DF] mb-2" />
<p className="text-sm text-slate-500">Total Users</p>
<p className="text-3xl font-bold text-[#1C323E]">{users.length}</p>
</CardContent>
</Card>
<Card className="border-slate-200">
<CardContent className="p-6">
<Shield className="w-8 h-8 text-red-600 mb-2" />
<p className="text-sm text-slate-500">Admins</p>
<p className="text-3xl font-bold text-red-600">
{users.filter(u => u.user_role === 'admin' || u.role === 'admin').length}
</p>
</CardContent>
</Card>
<Card className="border-slate-200">
<CardContent className="p-6">
<Building2 className="w-8 h-8 text-amber-600 mb-2" />
<p className="text-sm text-slate-500">Vendors</p>
<p className="text-3xl font-bold text-amber-600">
{users.filter(u => u.user_role === 'vendor').length}
</p>
</CardContent>
</Card>
<Card className="border-slate-200">
<CardContent className="p-6">
<Users className="w-8 h-8 text-blue-600 mb-2" />
<p className="text-sm text-slate-500">Workforce</p>
<p className="text-3xl font-bold text-blue-600">
{users.filter(u => u.user_role === 'workforce').length}
</p>
</CardContent>
</Card>
</div>
{/* Users List */}
<Card className="border-slate-200">
<CardHeader className="bg-gradient-to-br from-slate-50 to-white border-b">
<CardTitle>All Users</CardTitle>
</CardHeader>
<CardContent className="p-6">
<div className="space-y-4">
{users.map((user) => (
<div key={user.id} className="flex items-center justify-between p-4 bg-white border-2 border-slate-200 rounded-lg hover:border-[#0A39DF] transition-all">
<div className="flex items-center gap-4 flex-1">
<Avatar className="w-12 h-12 border-2 border-slate-200">
<AvatarImage src={user.profile_picture || sampleAvatar} alt={user.full_name} />
<AvatarFallback className="bg-gradient-to-br from-[#0A39DF] to-[#1C323E] text-white font-bold">
{user.full_name?.charAt(0) || user.email?.charAt(0) || '?'}
</AvatarFallback>
</Avatar>
<div className="flex-1">
<h4 className="font-semibold text-[#1C323E]">{user.full_name || 'Unnamed User'}</h4>
<div className="flex items-center gap-3 mt-1">
<span className="text-sm text-slate-500 flex items-center gap-1">
<Mail className="w-3 h-3" />
{user.email}
</span>
{user.company_name && (
<span className="text-sm text-slate-500 flex items-center gap-1">
<Building2 className="w-3 h-3" />
{user.company_name}
</span>
)}
</div>
</div>
<Badge className={getRoleColor(user.user_role || user.role)}>
{getRoleLabel(user.user_role || user.role)}
</Badge>
</div>
<div className="flex gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => handleEditPermissions(user)}
className="hover:text-[#0A39DF] hover:bg-blue-50"
title="Edit Permissions"
>
<Shield className="w-4 h-4" />
</Button>
<Button variant="outline" size="sm" title="Edit User">
<Edit className="w-4 h-4" />
</Button>
{/* Optionally add a delete button here */}
{/* <Button variant="outline" size="sm" className="text-red-500 hover:text-red-700 hover:bg-red-50" title="Delete User">
<Trash2 className="w-4 h-4" />
</Button> */}
</div>
</div>
))}
</div>
</CardContent>
</Card>
{/* Invite User Dialog */}
<Dialog open={showInviteDialog} onOpenChange={setShowInviteDialog}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Invite New User</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label>Full Name *</Label>
<Input
value={inviteData.full_name}
onChange={(e) => setInviteData({ ...inviteData, full_name: e.target.value })}
placeholder="John Doe"
/>
</div>
<div>
<Label>Email *</Label>
<Input
type="email"
value={inviteData.email}
onChange={(e) => setInviteData({ ...inviteData, email: e.target.value })}
placeholder="john@example.com"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label>Role *</Label>
<Select value={inviteData.user_role} onValueChange={(value) => setInviteData({ ...inviteData, user_role: value })}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="admin">Administrator</SelectItem>
<SelectItem value="procurement">Procurement</SelectItem>
<SelectItem value="operator">Operator</SelectItem>
<SelectItem value="sector">Sector Manager</SelectItem>
<SelectItem value="client">Client</SelectItem>
<SelectItem value="vendor">Vendor</SelectItem>
<SelectItem value="workforce">Workforce</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label>Phone</Label>
<Input
value={inviteData.phone}
onChange={(e) => setInviteData({ ...inviteData, phone: e.target.value })}
placeholder="(555) 123-4567"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label>Company Name</Label>
<Input
value={inviteData.company_name}
onChange={(e) => setInviteData({ ...inviteData, company_name: e.target.value })}
placeholder="Acme Corp"
/>
</div>
<div>
<Label>Department</Label>
<Input
value={inviteData.department}
onChange={(e) => setInviteData({ ...inviteData, department: e.target.value })}
placeholder="Operations"
/>
</div>
</div>
<div className="p-4 bg-blue-50 rounded-lg border border-blue-200">
<p className="text-sm text-blue-700">
<strong>Note:</strong> The user will receive an email invitation with instructions to set up their account and password.
</p>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowInviteDialog(false)}>
Cancel
</Button>
<Button onClick={handleInviteUser} className="bg-[#0A39DF]">
Send Invitation
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
{/* Permissions Modal */}
<UserPermissionsModal
user={selectedUser}
open={showPermissionsModal}
onClose={() => {
setShowPermissionsModal(false);
setSelectedUser(null);
}}
onSave={handleSavePermissions}
isSaving={updateUserMutation.isLoading}
/>
</div>
);
}