import React, { useState } from "react"; import { base44 } from "@/api/base44Client"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { Link, useNavigate } from "react-router-dom"; import { createPageUrl } from "@/utils"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { Users, Plus, Search, Building2, MapPin, UserCheck, Mail, Edit, Loader2, Trash2, UserX, LayoutGrid, List as ListIcon, RefreshCw, Send, Filter } from "lucide-react"; import PageHeader from "@/components/common/PageHeader"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { useToast } from "@/components/ui/use-toast"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; export default function Teams() { const navigate = useNavigate(); const queryClient = useQueryClient(); const { toast } = useToast(); const [searchTerm, setSearchTerm] = useState(""); const [departmentFilter, setDepartmentFilter] = useState("all"); const [viewMode, setViewMode] = useState("grid"); const [activeTab, setActiveTab] = useState("active"); const [showInviteMemberDialog, setShowInviteMemberDialog] = useState(false); const [showEditMemberDialog, setShowEditMemberDialog] = useState(false); const [showDepartmentDialog, setShowDepartmentDialog] = useState(false); const [showDeleteTeamDialog, setShowDeleteTeamDialog] = useState(false); const [editingMember, setEditingMember] = useState(null); const [editingDepartment, setEditingDepartment] = useState(null); const [teamToDelete, setTeamToDelete] = useState(null); const [newDepartment, setNewDepartment] = useState(""); const [inviteData, setInviteData] = useState({ email: "", full_name: "", role: "member", }); const { data: user } = useQuery({ queryKey: ['current-user-teams'], queryFn: () => base44.auth.me(), }); const userRole = user?.user_role || user?.role; /** * CRITICAL ISOLATION LAYER: * Each role (Admin, Procurement, Operator, Sector, Client, Vendor, Workforce) * gets their OWN isolated team that ONLY they can see and manage. * * Security Rules: * - Teams are filtered by owner_id === current user's ID * - Vendors CANNOT see Procurement teams * - Procurement CANNOT see Vendor teams * - Operators CANNOT see Client teams * - NO cross-layer visibility is allowed * * This ensures complete data isolation across all organizational layers. */ const { data: userTeam } = useQuery({ queryKey: ['user-team', user?.id, userRole], queryFn: async () => { if (!user?.id) { console.warn("⚠️ No user ID found - cannot fetch team"); return null; } // SECURITY: Fetch ALL teams and filter by owner_id // This ensures only THIS user's team is returned const allTeams = await base44.entities.Team.list('-created_date'); // Find ONLY teams owned by this specific user let team = allTeams.find(t => t.owner_id === user.id); // ISOLATION VERIFICATION if (team && team.owner_id !== user.id) { console.error("🚨 SECURITY VIOLATION: Team owner mismatch!"); return null; } // Auto-create team if doesn't exist (first time user accesses Teams) if (!team && user.id) { console.log(`✅ Creating new isolated team for ${userRole} user: ${user.email}`); const teamName = user.company_name || `${user.full_name}'s Team` || "My Team"; team = await base44.entities.Team.create({ team_name: teamName, owner_id: user.id, // CRITICAL: Links team to THIS user only owner_name: user.full_name || user.email, owner_role: userRole, // Tracks which layer this team belongs to email: user.email, phone: user.phone || "", total_members: 0, active_members: 0, total_hubs: 0, favorite_staff_count: 0, blocked_staff_count: 0, departments: [], // Initialize with an empty array for departments }); console.log(`✅ Team created successfully for ${userRole}: ${team.id}`); } // FINAL VERIFICATION: Ensure team belongs to current user if (team) { console.log(`🔒 Team loaded for ${userRole}: ${team.team_name} (Owner: ${team.owner_id})`); // Double-check ownership if (team.owner_id !== user.id) { console.error("🚨 CRITICAL: Attempted to load team not owned by current user!"); toast({ title: "⛔ Access Denied", description: "You don't have permission to view this team.", variant: "destructive", }); return null; } } return team; }, enabled: !!user?.id, }); /** * CRITICAL ISOLATION LAYER FOR TEAM MEMBERS: * Only fetch members that belong to THIS user's team. * Members from other teams/layers are NEVER visible. */ const { data: teamMembers = [] } = useQuery({ queryKey: ['team-members', userTeam?.id, user?.id], queryFn: async () => { if (!userTeam?.id) { console.log("⚠️ No team ID - returning empty members list"); return []; } // Fetch all members and filter by team_id const allMembers = await base44.entities.TeamMember.list('-created_date'); // SECURITY: Only return members that belong to THIS user's team const filteredMembers = allMembers.filter(m => m.team_id === userTeam.id); console.log(`🔒 Loaded ${filteredMembers.length} members for team ${userTeam.id}`); // VERIFICATION: Ensure all returned members belong to the correct team const invalidMembers = filteredMembers.filter(m => m.team_id !== userTeam.id); if (invalidMembers.length > 0) { console.error("🚨 SECURITY VIOLATION: Found members from other teams!"); return []; } return filteredMembers; }, enabled: !!userTeam?.id && !!user?.id, initialData: [], }); // Fetch pending invitations const { data: pendingInvites = [] } = useQuery({ queryKey: ['team-invites', userTeam?.id], queryFn: async () => { if (!userTeam?.id) return []; const allInvites = await base44.entities.TeamMemberInvite.list('-invited_date'); return allInvites.filter(inv => inv.team_id === userTeam.id && inv.invite_status === 'pending'); }, enabled: !!userTeam?.id, initialData: [], }); // Get unique departments from both team settings and existing team members const teamDepartments = userTeam?.departments || []; const memberDepartments = [...new Set(teamMembers.map(m => m.department).filter(Boolean))]; const uniqueDepartments = [...new Set([...teamDepartments, ...memberDepartments])]; // Separate active and deactivated members const activeMembers = teamMembers.filter(m => m.is_active); const deactivatedMembers = teamMembers.filter(m => !m.is_active); const createTestInviteMutation = useMutation({ mutationFn: async () => { if (!userTeam?.id) { throw new Error("Team not found. Cannot create test invite."); } if (!user?.email && !user?.full_name) { throw new Error("User identity not found. Cannot create test invite."); } const inviteCode = `TEAM-${Math.floor(10000 + Math.random() * 90000)}`; const invite = await base44.entities.TeamMemberInvite.create({ team_id: userTeam.id, team_name: userTeam.team_name || "Team", invite_code: inviteCode, email: "test@example.com", full_name: "Test User", role: "member", invited_by: user?.email || user?.full_name, invite_status: "pending", invited_date: new Date().toISOString(), expires_at: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString() }); return invite; }, onSuccess: (invite) => { navigate(createPageUrl("Onboarding") + `?invite=${invite.invite_code}`); }, onError: (error) => { toast({ title: "❌ Failed to Create Test Invite", description: error.message, variant: "destructive", }); }, }); const inviteMemberMutation = useMutation({ mutationFn: async (data) => { if (!userTeam?.id) { throw new Error("No team found. Please try refreshing the page."); } if (!user?.email && !user?.full_name) { throw new Error("Unable to identify who is sending the invite. Please try logging out and back in."); } const inviteCode = `TEAM-${Math.floor(10000 + Math.random() * 90000)}`; const invite = await base44.entities.TeamMemberInvite.create({ team_id: userTeam.id, team_name: userTeam.team_name || "Team", invite_code: inviteCode, email: data.email, full_name: data.full_name, role: data.role, invited_by: user?.email || user?.full_name, invite_status: "pending", invited_date: new Date().toISOString(), expires_at: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString() }); const registerUrl = `${window.location.origin}${createPageUrl('Onboarding')}?invite=${inviteCode}`; await base44.integrations.Core.SendEmail({ from_name: userTeam.team_name || "Team", to: data.email, subject: `You're invited to join ${userTeam.team_name || 'our team'}!`, body: `

🎉 Team Invitation

Join ${userTeam.team_name || 'our team'}

Hi ${data.full_name || 'there'},

${user?.full_name || user?.email} has invited you to join ${userTeam.team_name || 'our team'} as a ${data.role}.

Register & Join Team →

What to do next:

  1. Click the button above to register
  2. Create your account with this email (${data.email})
  3. You'll be automatically added to the team

⏰ Important: This invitation will expire in 7 days.

Your invite code: ${inviteCode}
Questions? Contact ${user?.email || 'the team admin'}

` }); return { invite }; }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['team-members', userTeam?.id] }); queryClient.invalidateQueries({ queryKey: ['team-invites', userTeam?.id] }); setShowInviteMemberDialog(false); setInviteData({ email: "", full_name: "", role: "member", }); toast({ title: "✅ Invitation Sent!", description: `Email invitation sent to ${inviteData.email}`, }); }, onError: (error) => { toast({ title: "❌ Failed to Send Invitation", description: error.message, variant: "destructive", }); }, }); const resendInviteMutation = useMutation({ mutationFn: async (invite) => { const registerUrl = `${window.location.origin}${createPageUrl('Onboarding')}?invite=${invite.invite_code}`; await base44.integrations.Core.SendEmail({ from_name: userTeam.team_name || "Team", to: invite.email, subject: `Reminder: You're invited to join ${userTeam.team_name || 'our team'}!`, body: `

📬 Reminder: Team Invitation

Join ${userTeam.team_name || 'our team'}

Hi ${invite.full_name || 'there'},

This is a reminder that ${invite.invited_by} has invited you to join ${userTeam.team_name || 'our team'} as a ${invite.role}.

Register & Join Team →

⏰ Important: This invitation will expire soon. Please register at your earliest convenience.

Your invite code: ${invite.invite_code}
Questions? Contact ${user?.email || 'the team admin'}

` }); return invite; }, onSuccess: () => { toast({ title: "✅ Invitation Resent!", description: "The invitation has been sent again", }); }, onError: (error) => { toast({ title: "❌ Failed to Resend", description: error.message, variant: "destructive", }); }, }); const updateMemberMutation = useMutation({ mutationFn: ({ id, data }) => base44.entities.TeamMember.update(id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['team-members', userTeam?.id] }); setShowEditMemberDialog(false); setEditingMember(null); toast({ title: "✅ Member Updated", description: "Team member updated successfully", }); }, }); const deactivateMemberMutation = useMutation({ mutationFn: ({ id }) => base44.entities.TeamMember.update(id, { is_active: false }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['team-members', userTeam?.id] }); toast({ title: "✅ Member Deactivated", description: "Team member has been deactivated", }); }, }); const activateMemberMutation = useMutation({ mutationFn: ({ id }) => base44.entities.TeamMember.update(id, { is_active: true }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['team-members', userTeam?.id] }); toast({ title: "✅ Member Activated", description: "Team member has been activated", }); }, }); const handleInviteMember = () => { inviteMemberMutation.mutate(inviteData); }; const handleEditMember = (member) => { setEditingMember(member); setShowEditMemberDialog(true); }; const handleUpdateMember = () => { updateMemberMutation.mutate({ id: editingMember.id, data: editingMember }); }; const handleDeactivateMember = (member) => { deactivateMemberMutation.mutate({ id: member.id }); }; const handleActivateMember = (member) => { activateMemberMutation.mutate({ id: member.id }); }; const handleAddDepartment = () => { setEditingDepartment(null); setNewDepartment(""); setShowDepartmentDialog(true); }; const handleSaveDepartment = async () => { if (!newDepartment.trim()) { toast({ title: "⚠️ Invalid Department", description: "Department name cannot be empty", variant: "destructive", }); return; } if (!userTeam?.id) { toast({ title: "⚠️ Error", description: "Team not found. Please refresh the page.", variant: "destructive", }); return; } try { const currentDepartments = userTeam.departments || []; let updatedDepartments; if (editingDepartment) { // Update existing department updatedDepartments = currentDepartments.map(dept => dept === editingDepartment ? newDepartment.trim() : dept ); } else { // Add new department if (currentDepartments.includes(newDepartment.trim())) { toast({ title: "⚠️ Duplicate Department", description: "This department already exists", variant: "destructive", }); return; } updatedDepartments = [...currentDepartments, newDepartment.trim()]; } // Update the team with new departments list await base44.entities.Team.update(userTeam.id, { departments: updatedDepartments }); // Refresh team data queryClient.invalidateQueries({ queryKey: ['user-team', user?.id, userRole] }); toast({ title: "✅ Department Saved", description: `Department "${newDepartment}" has been ${editingDepartment ? 'updated' : 'added'}`, }); setShowDepartmentDialog(false); setEditingDepartment(null); setNewDepartment(""); } catch (error) { toast({ title: "❌ Error", description: "Failed to save department. Please try again.", variant: "destructive", }); } }; const handleDeleteDepartment = async (deptToDelete) => { if (!userTeam?.id) return; try { const currentDepartments = userTeam.departments || []; const updatedDepartments = currentDepartments.filter(dept => dept !== deptToDelete); await base44.entities.Team.update(userTeam.id, { departments: updatedDepartments }); queryClient.invalidateQueries({ queryKey: ['user-team', user?.id, userRole] }); toast({ title: "✅ Department Deleted", description: `Department "${deptToDelete}" has been removed`, }); } catch (error) { toast({ title: "❌ Error", description: "Failed to delete department. Please try again.", variant: "destructive", }); } }; const filteredMembers = (members) => members.filter(member => { const matchesSearch = !searchTerm || member.member_name?.toLowerCase().includes(searchTerm.toLowerCase()) || member.email?.toLowerCase().includes(searchTerm.toLowerCase()) || member.title?.toLowerCase().includes(searchTerm.toLowerCase()); const matchesDepartment = departmentFilter === "all" || member.department === departmentFilter; return matchesSearch && matchesDepartment; }); const getRoleTitle = () => { const titles = { admin: "Admin Team", procurement: "Procurement Team", operator: "Operator Team", sector: "Sector Team", client: "Client Team", vendor: "Vendor Team", workforce: "Workforce Team" }; return titles[userRole] || "Team"; }; const getIsolatedSubtitle = () => { return `${activeMembers.length} active • ${deactivatedMembers.length} deactivated • ${pendingInvites.length} pending invites`; }; const renderMemberCard = (member) => (
{member.member_name?.split(' ').map(n => n[0]).join('') || '?'} {!member.is_active && (
)}

{member.member_name}

{member.role}

{member.title && (

{member.title}

)}
{member.email && (
{member.email}
)} {member.department && ( {member.department} )}
{member.is_active ? ( ) : ( )}
); return (
{/* Security Notice Banner */}

Isolated Team: You can only see and manage YOUR team members. {userRole === 'vendor' && " Procurement teams are NOT visible to you."} {userRole === 'procurement' && " Vendor teams are NOT visible to you."} {userRole === 'operator' && " Other layer teams are NOT visible to you."}

{/* Team Members Section */}
Team Management

Manage roles, permissions, and team organization

{['admin', 'procurement', 'operator', 'vendor'].includes(userRole) && ( )}
{/* Filters and View Toggle */}
setSearchTerm(e.target.value)} className="pl-10 border-slate-300 focus:border-[#0A39DF] transition-all" />
{/* Tabs for Active, Deactivated, and Invitations */} Active ({activeMembers.length}) Deactivated ({deactivatedMembers.length}) Invitations ({pendingInvites.length}) {/* Active Members Tab */} {viewMode === "grid" && filteredMembers(activeMembers).length > 0 && (
{filteredMembers(activeMembers).map(renderMemberCard)}
)} {viewMode === "list" && filteredMembers(activeMembers).length > 0 && (
{filteredMembers(activeMembers).map((member) => (
{member.member_name?.split(' ').map(n => n[0]).join('') || '?'}

{member.member_name}

{member.role} {member.title && `• ${member.title}`}

{member.phone && `${member.phone} • `}{member.email}

{member.department && ( {member.department} )}
))}
)} {filteredMembers(activeMembers).length === 0 && (

No Active Members

{activeMembers.length === 0 ? 'Invite your first team member to get started' : 'No active members match your filters'}

{activeMembers.length === 0 && ( )}
)}
{/* Deactivated Members Tab */} {viewMode === "grid" && filteredMembers(deactivatedMembers).length > 0 && (
{filteredMembers(deactivatedMembers).map(renderMemberCard)}
)} {viewMode === "list" && filteredMembers(deactivatedMembers).length > 0 && (
{filteredMembers(deactivatedMembers).map((member) => (
{member.member_name?.split(' ').map(n => n[0]).join('') || '?'}

{member.member_name}

{member.role} {member.title && `• ${member.title}`}

{member.phone && `${member.phone} • `}{member.email}

{member.department && ( {member.department} )}
))}
)} {filteredMembers(deactivatedMembers).length === 0 && (

No Deactivated Members

All your team members are currently active

)}
{/* Pending Invitations Tab */} {pendingInvites.length > 0 ? (
{pendingInvites.map((invite) => (

{invite.full_name}

{invite.email}

{invite.role} Invited {new Date(invite.invited_date).toLocaleDateString()}
))}
) : (

No Pending Invitations

All invitations have been accepted or expired

)}
{/* Invite Member Dialog */} Invite team member

Assign specific roles and permissions to control what each team member can access and manage.

{ const firstName = e.target.value; const lastName = inviteData.full_name.split(' ').slice(1).join(' '); setInviteData({ ...inviteData, full_name: `${firstName} ${lastName}`.trim() }); }} placeholder="" />
{ const firstName = inviteData.full_name.split(' ')[0] || ""; const lastName = e.target.value; setInviteData({ ...inviteData, full_name: `${firstName} ${lastName}`.trim() }); }} placeholder="" />
setInviteData({ ...inviteData, email: e.target.value })} placeholder="" />
{/* Department Dialog */} {editingDepartment ? 'Edit Department' : 'Add New Department'} Manage the list of departments available for your team members.
setNewDepartment(e.target.value)} placeholder="e.g., Operations, Sales, HR" className="mt-2" />

Current Departments:

{uniqueDepartments.length > 0 ? ( uniqueDepartments.map(dept => (
{dept}
)) ) : (

No departments added yet. Add your first department above.

)}
{/* Edit Member Dialog */} Edit Team Member {editingMember && (
setEditingMember({ ...editingMember, member_name: e.target.value })} placeholder="John Doe" />
setEditingMember({ ...editingMember, email: e.target.value })} placeholder="john@example.com" />
setEditingMember({ ...editingMember, phone: e.target.value })} placeholder="+1 (555) 123-4567" />
setEditingMember({ ...editingMember, title: e.target.value })} placeholder="Manager" />
)}
); }