export with one error
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
|
||||
import React, { useState } from "react";
|
||||
import React, { useState, useMemo } from "react";
|
||||
import { base44 } from "@/api/base44Client";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||
@@ -7,15 +6,51 @@ 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 { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
||||
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 {
|
||||
Users, UserPlus, Mail, Shield, Building2, Edit, Trash2, Search,
|
||||
Filter, MoreVertical, Eye, Key, UserCheck, UserX, Layers,
|
||||
Phone, Calendar, Clock, CheckCircle2, XCircle, AlertCircle
|
||||
} 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
|
||||
import UserPermissionsModal from "@/components/permissions/UserPermissionsModal";
|
||||
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
// Layer configuration
|
||||
const LAYERS = [
|
||||
{ id: "all", name: "All Users", icon: Users, color: "bg-slate-600" },
|
||||
{ id: "admin", name: "Admins", icon: Shield, color: "bg-red-600" },
|
||||
{ id: "procurement", name: "Procurement", icon: Building2, color: "bg-blue-600" },
|
||||
{ id: "operator", name: "Operators", icon: Building2, color: "bg-emerald-600" },
|
||||
{ id: "sector", name: "Sectors", icon: Layers, color: "bg-purple-600" },
|
||||
{ id: "client", name: "Clients", icon: Users, color: "bg-green-600" },
|
||||
{ id: "vendor", name: "Vendors", icon: Building2, color: "bg-amber-600" },
|
||||
{ id: "workforce", name: "Workforce", icon: Users, color: "bg-slate-500" },
|
||||
];
|
||||
|
||||
const ROLE_CONFIG = {
|
||||
admin: { name: "Administrator", color: "bg-red-100 text-red-700 border-red-200", bgGradient: "from-red-500 to-red-700" },
|
||||
procurement: { name: "Procurement", color: "bg-blue-100 text-blue-700 border-blue-200", bgGradient: "from-blue-500 to-blue-700" },
|
||||
operator: { name: "Operator", color: "bg-emerald-100 text-emerald-700 border-emerald-200", bgGradient: "from-emerald-500 to-emerald-700" },
|
||||
sector: { name: "Sector Manager", color: "bg-purple-100 text-purple-700 border-purple-200", bgGradient: "from-purple-500 to-purple-700" },
|
||||
client: { name: "Client", color: "bg-green-100 text-green-700 border-green-200", bgGradient: "from-green-500 to-green-700" },
|
||||
vendor: { name: "Vendor", color: "bg-amber-100 text-amber-700 border-amber-200", bgGradient: "from-amber-500 to-amber-700" },
|
||||
workforce: { name: "Workforce", color: "bg-slate-100 text-slate-700 border-slate-200", bgGradient: "from-slate-500 to-slate-700" },
|
||||
};
|
||||
|
||||
export default function UserManagement() {
|
||||
const [showInviteDialog, setShowInviteDialog] = useState(false);
|
||||
const [activeLayer, setActiveLayer] = useState("all");
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [inviteData, setInviteData] = useState({
|
||||
email: "",
|
||||
full_name: "",
|
||||
@@ -27,14 +62,14 @@ export default function UserManagement() {
|
||||
|
||||
const [selectedUser, setSelectedUser] = useState(null);
|
||||
const [showPermissionsModal, setShowPermissionsModal] = useState(false);
|
||||
const [showUserDetailModal, setShowUserDetailModal] = useState(false);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const { toast } = useToast();
|
||||
|
||||
const { data: users } = useQuery({
|
||||
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;
|
||||
},
|
||||
@@ -52,10 +87,10 @@ export default function UserManagement() {
|
||||
queryClient.invalidateQueries({ queryKey: ['all-users'] });
|
||||
toast({
|
||||
title: "User Updated",
|
||||
description: "User role and information updated successfully",
|
||||
description: "User information updated successfully",
|
||||
});
|
||||
setShowPermissionsModal(false); // Close the modal on success
|
||||
setSelectedUser(null); // Clear selected user
|
||||
setShowPermissionsModal(false);
|
||||
setSelectedUser(null);
|
||||
},
|
||||
onError: (error) => {
|
||||
toast({
|
||||
@@ -66,6 +101,39 @@ export default function UserManagement() {
|
||||
}
|
||||
});
|
||||
|
||||
// Calculate stats per layer
|
||||
const layerStats = useMemo(() => {
|
||||
const stats = {};
|
||||
LAYERS.forEach(layer => {
|
||||
if (layer.id === "all") {
|
||||
stats[layer.id] = users.length;
|
||||
} else {
|
||||
stats[layer.id] = users.filter(u => (u.user_role || u.role) === layer.id).length;
|
||||
}
|
||||
});
|
||||
return stats;
|
||||
}, [users]);
|
||||
|
||||
// Filter users
|
||||
const filteredUsers = useMemo(() => {
|
||||
let filtered = users;
|
||||
|
||||
if (activeLayer !== "all") {
|
||||
filtered = filtered.filter(u => (u.user_role || u.role) === activeLayer);
|
||||
}
|
||||
|
||||
if (searchTerm) {
|
||||
const term = searchTerm.toLowerCase();
|
||||
filtered = filtered.filter(u =>
|
||||
u.full_name?.toLowerCase().includes(term) ||
|
||||
u.email?.toLowerCase().includes(term) ||
|
||||
u.company_name?.toLowerCase().includes(term)
|
||||
);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}, [users, activeLayer, searchTerm]);
|
||||
|
||||
const handleInviteUser = async () => {
|
||||
if (!inviteData.email || !inviteData.full_name) {
|
||||
toast({
|
||||
@@ -76,8 +144,6 @@ export default function UserManagement() {
|
||||
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.`,
|
||||
@@ -94,49 +160,22 @@ export default function UserManagement() {
|
||||
});
|
||||
};
|
||||
|
||||
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
|
||||
}
|
||||
const handleViewUser = (user) => {
|
||||
setSelectedUser(user);
|
||||
setShowUserDetailModal(true);
|
||||
};
|
||||
|
||||
// Only admins can access this page
|
||||
const handleSavePermissions = async (updatedUser) => {
|
||||
await updateUserMutation.mutateAsync({ userId: updatedUser.id, data: updatedUser });
|
||||
};
|
||||
|
||||
const getRoleConfig = (role) => ROLE_CONFIG[role] || ROLE_CONFIG.workforce;
|
||||
|
||||
if (currentUser?.user_role !== "admin" && currentUser?.role !== "admin") {
|
||||
return (
|
||||
<div className="p-8 text-center">
|
||||
@@ -147,133 +186,240 @@ export default function UserManagement() {
|
||||
);
|
||||
}
|
||||
|
||||
// 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">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4 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>
|
||||
<h1 className="text-3xl font-bold text-slate-900">User Management</h1>
|
||||
<p className="text-slate-500 mt-1">Manage users across all ecosystem layers</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => setShowInviteDialog(true)}
|
||||
className="bg-[#0A39DF] hover:bg-[#0A39DF]/90"
|
||||
className="bg-gradient-to-r from-[#0A39DF] to-[#1C323E] shadow-lg"
|
||||
>
|
||||
<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>
|
||||
{/* Layer Stats Cards */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-8 gap-3 mb-6">
|
||||
{LAYERS.map((layer) => {
|
||||
const Icon = layer.icon;
|
||||
const isActive = activeLayer === layer.id;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={layer.id}
|
||||
onClick={() => setActiveLayer(layer.id)}
|
||||
className={`p-4 rounded-xl border-2 transition-all text-center ${
|
||||
isActive
|
||||
? 'border-[#0A39DF] bg-blue-50 shadow-md scale-105'
|
||||
: 'border-slate-200 bg-white hover:border-slate-300 hover:shadow-sm'
|
||||
}`}
|
||||
>
|
||||
<div className={`w-10 h-10 mx-auto rounded-lg ${layer.color} flex items-center justify-center mb-2`}>
|
||||
<Icon className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-slate-900">{layerStats[layer.id]}</p>
|
||||
<p className="text-xs text-slate-500 truncate">{layer.name}</p>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</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>
|
||||
))}
|
||||
{/* Search and Filter */}
|
||||
<Card className="mb-6 border-slate-200 shadow-sm">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex flex-col md:flex-row gap-4">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-slate-400" />
|
||||
<Input
|
||||
placeholder="Search by name, email, or company..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10 border-slate-300"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Select value={activeLayer} onValueChange={setActiveLayer}>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<Filter className="w-4 h-4 mr-2" />
|
||||
<SelectValue placeholder="Filter by layer" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{LAYERS.map((layer) => (
|
||||
<SelectItem key={layer.id} value={layer.id}>
|
||||
{layer.name} ({layerStats[layer.id]})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Users Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{filteredUsers.map((user) => {
|
||||
const role = user.user_role || user.role || "workforce";
|
||||
const config = getRoleConfig(role);
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={user.id}
|
||||
className="border-slate-200 hover:border-[#0A39DF] hover:shadow-lg transition-all overflow-hidden group"
|
||||
>
|
||||
{/* Role Header */}
|
||||
<div className={`h-2 bg-gradient-to-r ${config.bgGradient}`}></div>
|
||||
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-start gap-4">
|
||||
<Avatar className="w-14 h-14 border-2 border-slate-200 shadow-sm">
|
||||
<AvatarImage src={user.profile_picture || sampleAvatar} alt={user.full_name} />
|
||||
<AvatarFallback className={`bg-gradient-to-br ${config.bgGradient} text-white font-bold text-lg`}>
|
||||
{user.full_name?.charAt(0) || user.email?.charAt(0) || '?'}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div>
|
||||
<h4 className="font-bold text-slate-900 truncate">{user.full_name || 'Unnamed User'}</h4>
|
||||
<Badge className={`${config.color} border text-xs mt-1`}>
|
||||
{config.name}
|
||||
</Badge>
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<MoreVertical className="w-4 h-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => handleViewUser(user)}>
|
||||
<Eye className="w-4 h-4 mr-2" />
|
||||
View Details
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleEditPermissions(user)}>
|
||||
<Key className="w-4 h-4 mr-2" />
|
||||
Edit Permissions
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="text-red-600">
|
||||
<UserX className="w-4 h-4 mr-2" />
|
||||
Deactivate User
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 space-y-1.5">
|
||||
<p className="text-sm text-slate-600 flex items-center gap-2 truncate">
|
||||
<Mail className="w-3.5 h-3.5 text-slate-400 flex-shrink-0" />
|
||||
{user.email}
|
||||
</p>
|
||||
{user.company_name && (
|
||||
<p className="text-sm text-slate-600 flex items-center gap-2 truncate">
|
||||
<Building2 className="w-3.5 h-3.5 text-slate-400 flex-shrink-0" />
|
||||
{user.company_name}
|
||||
</p>
|
||||
)}
|
||||
{user.phone && (
|
||||
<p className="text-sm text-slate-600 flex items-center gap-2 truncate">
|
||||
<Phone className="w-3.5 h-3.5 text-slate-400 flex-shrink-0" />
|
||||
{user.phone}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="flex items-center gap-2 mt-4 pt-4 border-t border-slate-100">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1 text-xs"
|
||||
onClick={() => handleViewUser(user)}
|
||||
>
|
||||
<Eye className="w-3.5 h-3.5 mr-1" />
|
||||
View
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1 text-xs hover:bg-blue-50 hover:text-blue-700 hover:border-blue-300"
|
||||
onClick={() => handleEditPermissions(user)}
|
||||
>
|
||||
<Shield className="w-3.5 h-3.5 mr-1" />
|
||||
Permissions
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1 text-xs"
|
||||
>
|
||||
<Edit className="w-3.5 h-3.5 mr-1" />
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{filteredUsers.length === 0 && (
|
||||
<div className="text-center py-16 bg-white rounded-xl border border-slate-200">
|
||||
<Users className="w-16 h-16 mx-auto text-slate-300 mb-4" />
|
||||
<h3 className="text-xl font-semibold text-slate-900 mb-2">No users found</h3>
|
||||
<p className="text-slate-600">
|
||||
{searchTerm ? "Try adjusting your search" : "No users in this layer yet"}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Invite User Dialog */}
|
||||
<Dialog open={showInviteDialog} onOpenChange={setShowInviteDialog}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Invite New User</DialogTitle>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<UserPlus className="w-5 h-5 text-[#0A39DF]" />
|
||||
Invite New User
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
|
||||
<div className="space-y-6 py-4">
|
||||
{/* Role Selection */}
|
||||
<div>
|
||||
<Label className="text-sm font-semibold mb-3 block">Select User Role</Label>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
{Object.entries(ROLE_CONFIG).map(([roleId, config]) => (
|
||||
<button
|
||||
key={roleId}
|
||||
onClick={() => setInviteData({ ...inviteData, user_role: roleId })}
|
||||
className={`p-3 rounded-xl border-2 transition-all text-center ${
|
||||
inviteData.user_role === roleId
|
||||
? 'border-[#0A39DF] bg-blue-50 shadow-md'
|
||||
: 'border-slate-200 bg-white hover:border-slate-300'
|
||||
}`}
|
||||
>
|
||||
<div className={`w-8 h-8 mx-auto rounded-lg bg-gradient-to-br ${config.bgGradient} flex items-center justify-center mb-2`}>
|
||||
<Users className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
<p className="text-xs font-semibold text-slate-900">{config.name}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* User Details */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>Full Name *</Label>
|
||||
@@ -281,6 +427,7 @@ export default function UserManagement() {
|
||||
value={inviteData.full_name}
|
||||
onChange={(e) => setInviteData({ ...inviteData, full_name: e.target.value })}
|
||||
placeholder="John Doe"
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -290,86 +437,160 @@ export default function UserManagement() {
|
||||
value={inviteData.email}
|
||||
onChange={(e) => setInviteData({ ...inviteData, email: e.target.value })}
|
||||
placeholder="john@example.com"
|
||||
className="mt-1"
|
||||
/>
|
||||
</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"
|
||||
className="mt-1"
|
||||
/>
|
||||
</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"
|
||||
className="mt-1"
|
||||
/>
|
||||
</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>
|
||||
<Label>Department</Label>
|
||||
<Input
|
||||
value={inviteData.department}
|
||||
onChange={(e) => setInviteData({ ...inviteData, department: e.target.value })}
|
||||
placeholder="Operations"
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-blue-50 rounded-xl border border-blue-200">
|
||||
<div className="flex items-start gap-3">
|
||||
<Mail className="w-5 h-5 text-blue-600 mt-0.5" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-blue-900">Invitation will be sent</p>
|
||||
<p className="text-xs text-blue-700 mt-1">
|
||||
The user will receive an email with instructions to set up their account and access the platform as a <strong>{ROLE_CONFIG[inviteData.user_role].name}</strong>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowInviteDialog(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleInviteUser} className="bg-[#0A39DF]">
|
||||
<Button onClick={handleInviteUser} className="bg-gradient-to-r from-[#0A39DF] to-[#1C323E]">
|
||||
<Mail className="w-4 h-4 mr-2" />
|
||||
Send Invitation
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{/* Permissions Modal */}
|
||||
<UserPermissionsModal
|
||||
user={selectedUser}
|
||||
open={showPermissionsModal}
|
||||
onClose={() => {
|
||||
setShowPermissionsModal(false);
|
||||
setSelectedUser(null);
|
||||
}}
|
||||
onSave={handleSavePermissions}
|
||||
isSaving={updateUserMutation.isLoading}
|
||||
/>
|
||||
{/* User Detail Modal */}
|
||||
<Dialog open={showUserDetailModal} onOpenChange={setShowUserDetailModal}>
|
||||
<DialogContent className="max-w-lg">
|
||||
{selectedUser && (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle>User Details</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<Avatar className="w-20 h-20 border-2 border-slate-200">
|
||||
<AvatarImage src={selectedUser.profile_picture || sampleAvatar} alt={selectedUser.full_name} />
|
||||
<AvatarFallback className={`bg-gradient-to-br ${getRoleConfig(selectedUser.user_role || selectedUser.role).bgGradient} text-white font-bold text-2xl`}>
|
||||
{selectedUser.full_name?.charAt(0) || '?'}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<h3 className="text-xl font-bold text-slate-900">{selectedUser.full_name}</h3>
|
||||
<Badge className={`${getRoleConfig(selectedUser.user_role || selectedUser.role).color} border mt-1`}>
|
||||
{getRoleConfig(selectedUser.user_role || selectedUser.role).name}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg">
|
||||
<Mail className="w-5 h-5 text-slate-400" />
|
||||
<div>
|
||||
<p className="text-xs text-slate-500">Email</p>
|
||||
<p className="text-sm font-medium text-slate-900">{selectedUser.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
{selectedUser.phone && (
|
||||
<div className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg">
|
||||
<Phone className="w-5 h-5 text-slate-400" />
|
||||
<div>
|
||||
<p className="text-xs text-slate-500">Phone</p>
|
||||
<p className="text-sm font-medium text-slate-900">{selectedUser.phone}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{selectedUser.company_name && (
|
||||
<div className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg">
|
||||
<Building2 className="w-5 h-5 text-slate-400" />
|
||||
<div>
|
||||
<p className="text-xs text-slate-500">Company</p>
|
||||
<p className="text-sm font-medium text-slate-900">{selectedUser.company_name}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg">
|
||||
<Calendar className="w-5 h-5 text-slate-400" />
|
||||
<div>
|
||||
<p className="text-xs text-slate-500">Joined</p>
|
||||
<p className="text-sm font-medium text-slate-900">
|
||||
{selectedUser.created_date ? new Date(selectedUser.created_date).toLocaleDateString() : 'Unknown'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowUserDetailModal(false)}>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowUserDetailModal(false);
|
||||
handleEditPermissions(selectedUser);
|
||||
}}
|
||||
className="bg-[#0A39DF]"
|
||||
>
|
||||
<Shield className="w-4 h-4 mr-2" />
|
||||
Edit Permissions
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Permissions Modal */}
|
||||
<UserPermissionsModal
|
||||
user={selectedUser}
|
||||
open={showPermissionsModal}
|
||||
onClose={() => {
|
||||
setShowPermissionsModal(false);
|
||||
setSelectedUser(null);
|
||||
}}
|
||||
onSave={handleSavePermissions}
|
||||
isSaving={updateUserMutation.isPending}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user