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: `
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}.
⏰ Important: This invitation will expire in 7 days.
Your invite code: ${inviteCode}
Questions? Contact ${user?.email || 'the team admin'}
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}.
⏰ 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'}
{member.role}
{member.title && ({member.title}
)}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."}
Manage roles, permissions, and team organization
{member.role} {member.title && `• ${member.title}`}
{member.phone && `${member.phone} • `}{member.email}
{member.department && ({activeMembers.length === 0 ? 'Invite your first team member to get started' : 'No active members match your filters'}
{activeMembers.length === 0 && ( )}{member.role} {member.title && `• ${member.title}`}
{member.phone && `${member.phone} • `}{member.email}
{member.department && (All your team members are currently active
{invite.email}
All invitations have been accepted or expired