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:
375
frontend-web/src/pages/UserManagement.jsx
Normal file
375
frontend-web/src/pages/UserManagement.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user