Files
Krow-workspace/frontend-web/src/pages/Teams.jsx
bwnyasse 80cd49deb5 feat(Makefile): install frontend dependencies on dev command
feat(Makefile): patch Layout.jsx queryKey for local development
feat(frontend-web): mock base44 client for local development with role switching
feat(frontend-web): add event assignment modal with conflict detection and bulk assign
feat(frontend-web): add client dashboard with key metrics and quick actions
feat(frontend-web): add layout component with role-based navigation
feat(frontend-web): update various pages to use "@/components" alias
feat(frontend-web): update create event page with ai assistant toggle
feat(frontend-web): update dashboard page with new components
feat(frontend-web): update events page with quick assign popover
feat(frontend-web): update invite vendor page with hover card
feat(frontend-web): update messages page with conversation list and message thread
feat(frontend-web): update operator dashboard page with new components
feat(frontend-web): update partner management page with new components
feat(frontend-web): update permissions page with new components
feat(frontend-web): update procurement dashboard page with new components
feat(frontend-web): update smart vendor onboarding page with new components
feat(frontend-web): update staff directory page with new components
feat(frontend-web): update teams page with new components
feat(frontend-web): update user management page with new components
feat(frontend-web): update vendor compliance page with new components
feat(frontend-web): update main.jsx to include react query provider

feat: add vendor marketplace page
feat: add global import fix to prepare-export script
feat: add patch-layout-query-key script to fix query key
feat: update patch-base44-client script to use a more robust method
2025-11-13 14:56:31 -05:00

1274 lines
56 KiB
JavaScript

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: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; background: linear-gradient(to bottom, #f8fafc, #e0f2fe); border-radius: 12px;">
<div style="text-align: center; padding: 20px; background: linear-gradient(to right, #0A39DF, #1C323E); border-radius: 12px 12px 0 0;">
<h1 style="color: white; margin: 0;">🎉 Team Invitation</h1>
<p style="color: #e0f2fe; margin-top: 8px;">Join ${userTeam.team_name || 'our team'}</p>
</div>
<div style="padding: 30px; background: white; border-radius: 0 0 12px 12px;">
<p style="color: #1C323E; font-size: 16px; line-height: 1.6;">
Hi ${data.full_name || 'there'},
</p>
<p style="color: #475569; font-size: 14px; line-height: 1.6; margin-top: 16px;">
<strong>${user?.full_name || user?.email}</strong> has invited you to join <strong>${userTeam.team_name || 'our team'}</strong> as a <strong>${data.role}</strong>.
</p>
<div style="text-align: center; margin: 32px 0;">
<a href="${registerUrl}"
style="display: inline-block; padding: 16px 32px; background: linear-gradient(to right, #0A39DF, #1C323E);
color: white; text-decoration: none; border-radius: 8px; font-weight: bold; font-size: 16px;
box-shadow: 0 4px 6px rgba(10, 57, 223, 0.3);">
Register & Join Team →
</a>
</div>
<div style="background: #f1f5f9; padding: 16px; border-radius: 8px; margin: 24px 0; border-left: 4px solid #0A39DF;">
<h3 style="color: #1C323E; margin: 0 0 8px 0; font-size: 14px;">What to do next:</h3>
<ol style="color: #475569; margin: 0; padding-left: 20px; line-height: 1.8; font-size: 13px;">
<li>Click the button above to register</li>
<li>Create your account with this email (${data.email})</li>
<li>You'll be automatically added to the team</li>
</ol>
</div>
<div style="background: #fef3c7; padding: 16px; border-radius: 8px; margin-top: 24px; border: 1px solid #fbbf24;">
<p style="color: #92400e; font-size: 13px; margin: 0;">
<strong>⏰ Important:</strong> This invitation will expire in 7 days.
</p>
</div>
<p style="color: #64748b; font-size: 13px; margin-top: 32px; padding-top: 20px; border-top: 1px solid #e2e8f0;">
Your invite code: <strong>${inviteCode}</strong><br>
Questions? Contact ${user?.email || 'the team admin'}
</p>
</div>
</div>
`
});
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: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; background: linear-gradient(to bottom, #f8fafc, #e0f2fe); border-radius: 12px;">
<div style="text-align: center; padding: 20px; background: linear-gradient(to right, #0A39DF, #1C323E); border-radius: 12px 12px 0 0;">
<h1 style="color: white; margin: 0;">📬 Reminder: Team Invitation</h1>
<p style="color: #e0f2fe; margin-top: 8px;">Join ${userTeam.team_name || 'our team'}</p>
</div>
<div style="padding: 30px; background: white; border-radius: 0 0 12px 12px;">
<p style="color: #1C323E; font-size: 16px; line-height: 1.6;">
Hi ${invite.full_name || 'there'},
</p>
<p style="color: #475569; font-size: 14px; line-height: 1.6; margin-top: 16px;">
This is a reminder that <strong>${invite.invited_by}</strong> has invited you to join <strong>${userTeam.team_name || 'our team'}</strong> as a <strong>${invite.role}</strong>.
</p>
<div style="text-align: center; margin: 32px 0;">
<a href="${registerUrl}"
style="display: inline-block; padding: 16px 32px; background: linear-gradient(to right, #0A39DF, #1C323E);
color: white; text-decoration: none; border-radius: 8px; font-weight: bold; font-size: 16px;
box-shadow: 0 4px 6px rgba(10, 57, 223, 0.3);">
Register & Join Team →
</a>
</div>
<div style="background: #dbeafe; padding: 16px; border-radius: 8px; margin-top: 24px; border: 1px solid #3b82f6;">
<p style="color: #1e40af; font-size: 13px; margin: 0;">
<strong>⏰ Important:</strong> This invitation will expire soon. Please register at your earliest convenience.
</p>
</div>
<p style="color: #64748b; font-size: 13px; margin-top: 32px; padding-top: 20px; border-top: 1px solid #e2e8f0;">
Your invite code: <strong>${invite.invite_code}</strong><br>
Questions? Contact ${user?.email || 'the team admin'}
</p>
</div>
</div>
`
});
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) => (
<Card
key={member.id}
className={`group border-2 transition-all duration-300 ${
member.is_active
? 'border-slate-200 hover:border-[#0A39DF] hover:shadow-2xl hover:-translate-y-1'
: 'border-slate-200 bg-slate-50/50 opacity-70'
}`}
>
<CardContent className="p-6">
<div className="flex items-center gap-4 mb-4">
<div className={`relative w-14 h-14 rounded-xl flex items-center justify-center text-white font-bold text-xl shadow-lg ${
member.is_active
? 'bg-gradient-to-br from-[#0A39DF] to-[#1C323E]'
: 'bg-gradient-to-br from-slate-400 to-slate-600'
}`}>
{member.member_name?.split(' ').map(n => n[0]).join('') || '?'}
{!member.is_active && (
<div className="absolute -top-1 -right-1 w-6 h-6 bg-red-500 rounded-full flex items-center justify-center ring-2 ring-white">
<UserX className="w-3 h-3 text-white" />
</div>
)}
</div>
<div className="flex-1 min-w-0">
<h4 className="font-bold text-[#1C323E] text-lg group-hover:text-[#0A39DF] transition-colors truncate">
{member.member_name}
</h4>
<p className="text-sm text-slate-600 capitalize">{member.role}</p>
{member.title && (
<p className="text-xs text-slate-500 mt-0.5 truncate">{member.title}</p>
)}
</div>
</div>
<div className="space-y-2 text-sm mb-4">
{member.email && (
<div className="flex items-center gap-2 text-slate-600 hover:text-[#0A39DF] transition-colors">
<Mail className="w-3 h-3 flex-shrink-0" />
<span className="truncate">{member.email}</span>
</div>
)}
{member.department && (
<Badge variant="outline" className="text-xs bg-blue-50 text-blue-700 border-blue-200">
{member.department}
</Badge>
)}
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
className="flex-1 hover:bg-slate-50 hover:border-[#0A39DF] hover:text-[#0A39DF] transition-all"
onClick={() => handleEditMember(member)}
>
<Edit className="w-3 h-3 mr-1" />
Edit
</Button>
{member.is_active ? (
<Button
variant="outline"
size="sm"
className="text-red-600 hover:bg-red-50 hover:border-red-300 transition-all"
onClick={() => handleDeactivateMember(member)}
>
<UserX className="w-3 h-3" />
</Button>
) : (
<Button
variant="outline"
size="sm"
className="text-green-600 hover:bg-green-50 hover:border-green-300 transition-all"
onClick={() => handleActivateMember(member)}
>
<UserCheck className="w-3 h-3" />
</Button>
)}
</div>
</CardContent>
</Card>
);
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100">
<div className="p-4 md:p-8">
<div className="max-w-7xl mx-auto">
{/* Security Notice Banner */}
<div className="mb-6 p-4 bg-gradient-to-r from-blue-50 to-indigo-50 border-l-4 border-[#0A39DF] rounded-lg shadow-sm">
<p className="text-sm text-blue-900 flex items-center gap-2">
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clipRule="evenodd" />
</svg>
<strong>Isolated Team:</strong> 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."}
</p>
</div>
<PageHeader
title={getRoleTitle()}
subtitle={getIsolatedSubtitle()}
/>
{/* Team Members Section */}
<Card className="mb-6 border-slate-200 shadow-xl">
<CardHeader className="bg-gradient-to-br from-white to-slate-50 border-b border-slate-100">
<div className="flex items-center justify-between flex-wrap gap-4">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-[#0A39DF] to-[#1C323E] flex items-center justify-center shadow-lg">
<UserCheck className="w-6 h-6 text-white" />
</div>
<div>
<CardTitle className="text-xl text-[#1C323E]">Team Management</CardTitle>
<p className="text-sm text-slate-500 mt-1">Manage roles, permissions, and team organization</p>
</div>
</div>
<div className="flex gap-2 flex-wrap">
<Button
variant="outline"
onClick={handleAddDepartment}
className="hover:bg-slate-50 hover:border-[#0A39DF] hover:text-[#0A39DF] transition-all"
>
<Plus className="w-4 h-4 mr-2" />
Departments
</Button>
{['admin', 'procurement', 'operator', 'vendor'].includes(userRole) && (
<Button
variant="outline"
onClick={() => createTestInviteMutation.mutate()}
disabled={createTestInviteMutation.isPending || !userTeam?.id}
className="hover:bg-slate-50 hover:border-[#0A39DF] hover:text-[#0A39DF] transition-all"
>
{createTestInviteMutation.isPending ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Creating...
</>
) : (
"View Onboarding"
)}
</Button>
)}
<Button
className="bg-gradient-to-r from-[#0A39DF] to-[#1C323E] hover:from-[#0A39DF]/90 hover:to-[#1C323E]/90 text-white shadow-lg transition-all"
onClick={() => setShowInviteMemberDialog(true)}
>
<Mail className="w-4 h-4 mr-2" />
Invite Member
</Button>
</div>
</div>
</CardHeader>
<CardContent className="p-6">
{/* Filters and View Toggle */}
<div className="flex items-center gap-4 mb-6 flex-wrap">
<div className="relative flex-1 min-w-[240px]">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-slate-400" />
<Input
placeholder="Search team members..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10 border-slate-300 focus:border-[#0A39DF] transition-all"
/>
</div>
<div className="flex items-center gap-2">
<Filter className="w-4 h-4 text-slate-500" />
<Select value={departmentFilter} onValueChange={setDepartmentFilter}>
<SelectTrigger className="w-48 border-slate-300">
<SelectValue placeholder="All Departments" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Departments</SelectItem>
{uniqueDepartments.map(dept => (
<SelectItem key={dept} value={dept}>{dept}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-1 bg-slate-100 p-1 rounded-lg shadow-sm">
<Button
size="sm"
variant={viewMode === "grid" ? "default" : "ghost"}
onClick={() => setViewMode("grid")}
className={viewMode === "grid" ? "bg-[#0A39DF] hover:bg-[#0A39DF]/90 shadow-sm" : "hover:bg-white"}
>
<LayoutGrid className="w-4 h-4" />
</Button>
<Button
size="sm"
variant={viewMode === "list" ? "default" : "ghost"}
onClick={() => setViewMode("list")}
className={viewMode === "list" ? "bg-[#0A39DF] hover:bg-[#0A39DF]/90 shadow-sm" : "hover:bg-white"}
>
<ListIcon className="w-4 h-4" />
</Button>
</div>
</div>
{/* Tabs for Active, Deactivated, and Invitations */}
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid w-full grid-cols-3 mb-6 bg-slate-100 p-1">
<TabsTrigger value="active" className="data-[state=active]:bg-white data-[state=active]:shadow-sm flex items-center gap-2">
<UserCheck className="w-4 h-4" />
Active ({activeMembers.length})
</TabsTrigger>
<TabsTrigger value="deactivated" className="data-[state=active]:bg-white data-[state=active]:shadow-sm flex items-center gap-2">
<UserX className="w-4 h-4" />
Deactivated ({deactivatedMembers.length})
</TabsTrigger>
<TabsTrigger value="invitations" className="data-[state=active]:bg-white data-[state=active]:shadow-sm flex items-center gap-2">
<Mail className="w-4 h-4" />
Invitations ({pendingInvites.length})
</TabsTrigger>
</TabsList>
{/* Active Members Tab */}
<TabsContent value="active">
{viewMode === "grid" && filteredMembers(activeMembers).length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredMembers(activeMembers).map(renderMemberCard)}
</div>
)}
{viewMode === "list" && filteredMembers(activeMembers).length > 0 && (
<div className="space-y-3">
{filteredMembers(activeMembers).map((member) => (
<div
key={member.id}
className="flex items-center justify-between p-5 rounded-xl border-2 border-slate-200 hover:border-[#0A39DF] transition-all hover:shadow-lg bg-white"
>
<div className="flex items-center gap-4 flex-1 min-w-0">
<div className="w-14 h-14 bg-gradient-to-br from-[#0A39DF] to-[#1C323E] rounded-xl flex items-center justify-center text-white font-bold text-xl shadow-lg flex-shrink-0">
{member.member_name?.split(' ').map(n => n[0]).join('') || '?'}
</div>
<div className="flex-1 min-w-0">
<h4 className="font-bold text-[#1C323E] text-lg truncate">{member.member_name}</h4>
<p className="text-sm text-slate-600 capitalize">{member.role} {member.title && `${member.title}`}</p>
<p className="text-xs text-slate-500 mt-1 truncate">
{member.phone && `${member.phone}`}{member.email}
</p>
{member.department && (
<Badge variant="outline" className="mt-2 text-xs bg-blue-50 text-blue-700 border-blue-200">
{member.department}
</Badge>
)}
</div>
</div>
<div className="flex gap-2 flex-shrink-0">
<Button
variant="outline"
size="sm"
onClick={() => handleEditMember(member)}
className="hover:bg-slate-50 hover:border-[#0A39DF] hover:text-[#0A39DF] transition-all"
>
<Edit className="w-4 h-4 mr-2" />
Edit
</Button>
<Button
variant="outline"
size="sm"
className="text-red-600 hover:bg-red-50 hover:border-red-300 transition-all"
onClick={() => handleDeactivateMember(member)}
>
<UserX className="w-4 h-4 mr-2" />
Deactivate
</Button>
</div>
</div>
))}
</div>
)}
{filteredMembers(activeMembers).length === 0 && (
<div className="text-center py-16 bg-white rounded-xl border-2 border-dashed border-slate-200">
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-gradient-to-br from-slate-100 to-slate-200 flex items-center justify-center">
<UserCheck className="w-8 h-8 text-slate-400" />
</div>
<h3 className="text-xl font-semibold text-slate-700 mb-2">No Active Members</h3>
<p className="text-slate-500 mb-6">
{activeMembers.length === 0
? 'Invite your first team member to get started'
: 'No active members match your filters'}
</p>
{activeMembers.length === 0 && (
<Button
className="bg-gradient-to-r from-[#0A39DF] to-[#1C323E] hover:from-[#0A39DF]/90 hover:to-[#1C323E]/90 shadow-lg"
onClick={() => setShowInviteMemberDialog(true)}
>
<Mail className="w-4 h-4 mr-2" />
Invite First Member
</Button>
)}
</div>
)}
</TabsContent>
{/* Deactivated Members Tab */}
<TabsContent value="deactivated">
{viewMode === "grid" && filteredMembers(deactivatedMembers).length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredMembers(deactivatedMembers).map(renderMemberCard)}
</div>
)}
{viewMode === "list" && filteredMembers(deactivatedMembers).length > 0 && (
<div className="space-y-3">
{filteredMembers(deactivatedMembers).map((member) => (
<div
key={member.id}
className="flex items-center justify-between p-5 rounded-xl border-2 border-slate-200 bg-slate-50/50 opacity-70 hover:shadow-lg transition-all"
>
<div className="flex items-center gap-4 flex-1 min-w-0">
<div className="w-14 h-14 bg-gradient-to-br from-slate-400 to-slate-600 rounded-xl flex items-center justify-center text-white font-bold text-xl shadow-lg flex-shrink-0 relative">
{member.member_name?.split(' ').map(n => n[0]).join('') || '?'}
<div className="absolute -top-1 -right-1 w-6 h-6 bg-red-500 rounded-full flex items-center justify-center ring-2 ring-white">
<UserX className="w-3 h-3 text-white" />
</div>
</div>
<div className="flex-1 min-w-0">
<h4 className="font-bold text-[#1C323E] text-lg truncate">{member.member_name}</h4>
<p className="text-sm text-slate-600 capitalize">{member.role} {member.title && `${member.title}`}</p>
<p className="text-xs text-slate-500 mt-1 truncate">
{member.phone && `${member.phone}`}{member.email}
</p>
{member.department && (
<Badge variant="outline" className="mt-2 text-xs bg-blue-50 text-blue-700 border-blue-200">
{member.department}
</Badge>
)}
</div>
</div>
<div className="flex gap-2 flex-shrink-0">
<Button
variant="outline"
size="sm"
onClick={() => handleEditMember(member)}
className="hover:bg-slate-50 hover:border-[#0A39DF] hover:text-[#0A39DF] transition-all"
>
<Edit className="w-4 h-4 mr-2" />
Edit
</Button>
<Button
variant="outline"
size="sm"
className="text-green-600 hover:bg-green-50 hover:border-green-300 transition-all"
onClick={() => handleActivateMember(member)}
>
<UserCheck className="w-4 h-4 mr-2" />
Activate
</Button>
</div>
</div>
))}
</div>
)}
{filteredMembers(deactivatedMembers).length === 0 && (
<div className="text-center py-16 bg-white rounded-xl border-2 border-dashed border-slate-200">
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-gradient-to-br from-green-100 to-emerald-200 flex items-center justify-center">
<UserCheck className="w-8 h-8 text-green-600" />
</div>
<h3 className="text-xl font-semibold text-slate-700 mb-2">No Deactivated Members</h3>
<p className="text-slate-500">All your team members are currently active</p>
</div>
)}
</TabsContent>
{/* Pending Invitations Tab */}
<TabsContent value="invitations">
{pendingInvites.length > 0 ? (
<div className="space-y-3">
{pendingInvites.map((invite) => (
<div
key={invite.id}
className="flex items-center justify-between p-5 rounded-xl border-2 border-blue-200 bg-gradient-to-r from-blue-50 to-indigo-50 hover:shadow-lg transition-all"
>
<div className="flex items-center gap-4 flex-1 min-w-0">
<div className="w-14 h-14 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-xl flex items-center justify-center text-white shadow-lg flex-shrink-0">
<Mail className="w-7 h-7" />
</div>
<div className="flex-1 min-w-0">
<h4 className="font-bold text-[#1C323E] truncate">{invite.full_name}</h4>
<p className="text-sm text-slate-600 truncate">{invite.email}</p>
<div className="flex gap-2 mt-2 flex-wrap">
<Badge variant="outline" className="text-xs capitalize bg-white border-blue-300 text-blue-700">
{invite.role}
</Badge>
<Badge className="bg-blue-100 text-blue-800 text-xs">
Invited {new Date(invite.invited_date).toLocaleDateString()}
</Badge>
</div>
</div>
</div>
<Button
variant="outline"
size="sm"
onClick={() => resendInviteMutation.mutate(invite)}
disabled={resendInviteMutation.isPending}
className="text-[#0A39DF] border-[#0A39DF] hover:bg-[#0A39DF] hover:text-white transition-all flex-shrink-0"
>
{resendInviteMutation.isPending ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Sending...
</>
) : (
<>
<Send className="w-4 h-4 mr-2" />
Resend
</>
)}
</Button>
</div>
))}
</div>
) : (
<div className="text-center py-16 bg-white rounded-xl border-2 border-dashed border-slate-200">
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-gradient-to-br from-blue-100 to-indigo-200 flex items-center justify-center">
<Mail className="w-8 h-8 text-blue-600" />
</div>
<h3 className="text-xl font-semibold text-slate-700 mb-2">No Pending Invitations</h3>
<p className="text-slate-500 mb-6">All invitations have been accepted or expired</p>
</div>
)}
</TabsContent>
</Tabs>
</CardContent>
</Card>
{/* Invite Member Dialog */}
<Dialog open={showInviteMemberDialog} onOpenChange={setShowInviteMemberDialog}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle className="text-xl font-bold">Invite team member</DialogTitle>
<p className="text-sm text-slate-500 mt-2">
Assign specific roles and permissions to control what each team member can access and manage.
</p>
</DialogHeader>
<div className="grid grid-cols-2 gap-4">
<div>
<Label>First name</Label>
<Input
value={inviteData.full_name.split(' ')[0] || ""}
onChange={(e) => {
const firstName = e.target.value;
const lastName = inviteData.full_name.split(' ').slice(1).join(' ');
setInviteData({ ...inviteData, full_name: `${firstName} ${lastName}`.trim() });
}}
placeholder=""
/>
</div>
<div>
<Label>Last name</Label>
<Input
value={inviteData.full_name.split(' ').slice(1).join(' ') || ""}
onChange={(e) => {
const firstName = inviteData.full_name.split(' ')[0] || "";
const lastName = e.target.value;
setInviteData({ ...inviteData, full_name: `${firstName} ${lastName}`.trim() });
}}
placeholder=""
/>
</div>
<div>
<Label>Email</Label>
<Input
type="email"
value={inviteData.email}
onChange={(e) => setInviteData({ ...inviteData, email: e.target.value })}
placeholder=""
/>
</div>
<div>
<Label>Access level</Label>
<Select value={inviteData.role} onValueChange={(value) => setInviteData({ ...inviteData, role: value })}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="manager">Manager</SelectItem>
<SelectItem value="member">Member</SelectItem>
<SelectItem value="viewer">Viewer</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<DialogFooter className="mt-6">
<Button variant="outline" onClick={() => setShowInviteMemberDialog(false)}>Cancel</Button>
<Button
onClick={handleInviteMember}
className="bg-[#0A39DF] hover:bg-[#0A39DF]/90"
disabled={!inviteData.email || !inviteData.full_name || inviteMemberMutation.isPending}
>
{inviteMemberMutation.isPending ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Sending invitation...
</>
) : (
"Send invitation"
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Department Dialog */}
<Dialog open={showDepartmentDialog} onOpenChange={setShowDepartmentDialog}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>{editingDepartment ? 'Edit Department' : 'Add New Department'}</DialogTitle>
<DialogDescription>
Manage the list of departments available for your team members.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Department Name</Label>
<Input
value={newDepartment}
onChange={(e) => setNewDepartment(e.target.value)}
placeholder="e.g., Operations, Sales, HR"
className="mt-2"
/>
</div>
<div className="bg-slate-50 p-4 rounded-lg">
<h4 className="font-semibold text-sm mb-3">Current Departments:</h4>
<div className="flex flex-wrap gap-2">
{uniqueDepartments.length > 0 ? (
uniqueDepartments.map(dept => (
<div key={dept} className="flex items-center gap-2 bg-white px-3 py-2 rounded-lg border border-slate-200 shadow-sm">
<span className="text-sm font-medium">{dept}</span>
<div className="flex gap-1">
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 hover:bg-blue-50 hover:text-[#0A39DF]"
onClick={() => {
setEditingDepartment(dept);
setNewDepartment(dept);
}}
>
<Edit className="w-3 h-3" />
</Button>
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 hover:bg-red-50 hover:text-red-600"
onClick={() => handleDeleteDepartment(dept)}
>
<Trash2 className="w-3 h-3" />
</Button>
</div>
</div>
))
) : (
<p className="text-sm text-slate-500">No departments added yet. Add your first department above.</p>
)}
</div>
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => {
setShowDepartmentDialog(false);
setEditingDepartment(null);
setNewDepartment("");
}}
>
Cancel
</Button>
<Button
onClick={handleSaveDepartment}
className="bg-[#0A39DF] hover:bg-[#0A39DF]/90"
disabled={!newDepartment.trim()}
>
{editingDepartment ? 'Update' : 'Add'} Department
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Edit Member Dialog */}
<Dialog open={showEditMemberDialog} onOpenChange={setShowEditMemberDialog}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Edit Team Member</DialogTitle>
</DialogHeader>
{editingMember && (
<div className="grid grid-cols-2 gap-4">
<div>
<Label>Full Name *</Label>
<Input
value={editingMember.member_name}
onChange={(e) => setEditingMember({ ...editingMember, member_name: e.target.value })}
placeholder="John Doe"
/>
</div>
<div>
<Label>Email *</Label>
<Input
type="email"
value={editingMember.email}
onChange={(e) => setEditingMember({ ...editingMember, email: e.target.value })}
placeholder="john@example.com"
/>
</div>
<div>
<Label>Phone</Label>
<Input
value={editingMember.phone || ""}
onChange={(e) => setEditingMember({ ...editingMember, phone: e.target.value })}
placeholder="+1 (555) 123-4567"
/>
</div>
<div>
<Label>Title</Label>
<Input
value={editingMember.title || ""}
onChange={(e) => setEditingMember({ ...editingMember, title: e.target.value })}
placeholder="Manager"
/>
</div>
<div>
<Label>Role</Label>
<Select value={editingMember.role} onValueChange={(value) => setEditingMember({ ...editingMember, role: value })}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="manager">Manager</SelectItem>
<SelectItem value="member">Member</SelectItem>
<SelectItem value="viewer">Viewer</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label>Department</Label>
<Select
value={editingMember.department || ""}
onValueChange={(value) => setEditingMember({ ...editingMember, department: value })}
>
<SelectTrigger>
<SelectValue placeholder="Select department" />
</SelectTrigger>
<SelectContent>
{uniqueDepartments.map(dept => (
<SelectItem key={dept} value={dept}>{dept}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={() => setShowEditMemberDialog(false)}>Cancel</Button>
<Button
onClick={handleUpdateMember}
className="bg-[#0A39DF]"
disabled={!editingMember?.member_name || !editingMember?.email || updateMemberMutation.isPending}
>
{updateMemberMutation.isPending ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Updating...
</>
) : (
<>
<Edit className="w-4 h-4 mr-2" />
Update Member
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</div>
</div>
);
}