Files
Krow-workspace/frontend-web-free/src/pages/Teams.jsx
2025-12-18 09:50:54 -05:00

2482 lines
123 KiB
JavaScript

import React, { useState, useEffect } 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 { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Users, Plus, Search, Building2, MapPin, UserCheck, Mail, Edit, Loader2, Trash2, UserX, LayoutGrid, List as ListIcon, RefreshCw, Send, Filter, Star, UserPlus } 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 [favoriteSearch, setFavoriteSearch] = useState("");
const [blockedSearch, setBlockedSearch] = useState("");
const [showAddFavoriteDialog, setShowAddFavoriteDialog] = useState(false);
const [showAddBlockedDialog, setShowAddBlockedDialog] = useState(false);
const [blockReason, setBlockReason] = useState("");
const [showAddHubDialog, setShowAddHubDialog] = useState(false);
const [showAddHubDepartmentDialog, setShowAddHubDepartmentDialog] = useState(false);
const [selectedHubForDept, setSelectedHubForDept] = useState(null);
const [preSelectedHub, setPreSelectedHub] = useState(null);
const [newHubDepartment, setNewHubDepartment] = useState({
department_name: "",
cost_center: ""
});
const [inviteData, setInviteData] = useState({
email: "",
full_name: "",
role: "member",
hub: "",
department: "",
});
const [newHub, setNewHub] = useState({
hub_name: "",
address: "",
manager_name: "",
manager_position: "",
manager_email: ""
});
const [isGoogleMapsLoaded, setIsGoogleMapsLoaded] = useState(false);
const addressInputRef = React.useRef(null);
const autocompleteRef = React.useRef(null);
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 () => {debugger;
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.ownerId === user.id);
console.log( team);
// ISOLATION VERIFICATION
if (team && team.ownerId !== 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.companyName || `${user.fullName}'s Team` || "My Team";
try {
team = await base44.entities.Team.create({
teamName: teamName,
ownerId: user.id, // CRITICAL: Links team to THIS user only
ownerName: user.fullName || user.email,
ownerRole: userRole, // Tracks which layer this team belongs to
//email: user.email,
//phone: user.phone || "",
//total_members: 0,
//active_members: 0,
//total_hubs: 0,
favoriteStaff: 0,
blockedStaff: 0,
//departments: [], // Initialize with an empty array for departments
});
console.log(`✅ Team created successfully for ${userRole}: ${team.id}`);
} catch (err) {
console.error("🔥 EXCEPTION in Team.list:", err);
}
}
// FINAL VERIFICATION: Ensure team belongs to current user
if (team) {
console.log(`🔒 Team loaded for ${userRole}: ${team.teamName} (Owner: ${team.ownerId})`);
// Double-check ownership
if (team.ownerId !== 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: [],
});
const { data: allStaff = [] } = useQuery({
queryKey: ['staff-for-favorites'],
queryFn: () => base44.entities.Staff.list(),
enabled: !!userTeam?.id,
initialData: [],
});
const { data: teamHubs = [] } = useQuery({
queryKey: ['team-hubs-main', userTeam?.id],
queryFn: async () => {
if (!userTeam?.id) return [];
const allHubs = await base44.entities.TeamHub.list('-created_date');
return allHubs.filter(h => h.team_id === userTeam.id);
},
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 !== false);
const deactivatedMembers = teamMembers.filter(m => m.is_active === false);
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)}`;
// Use the first hub if available, or empty string
const firstHub = teamHubs.length > 0 ? teamHubs[0].hub_name : "";
const firstDept = uniqueDepartments.length > 0 ? uniqueDepartments[0] : "Operations";
const invite = await base44.entities.TeamMemberInvite.create({
team_id: userTeam.id,
team_name: userTeam.team_name || "Team",
invite_code: inviteCode,
email: "demo@example.com",
full_name: "Demo User",
role: "member",
hub: firstHub,
department: firstDept,
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.");
}
// Create hub if it doesn't exist, or update hub with new department
const existingHub = teamHubs.find(h => h.hub_name === data.hub);
if (data.hub && !existingHub) {
// Create new hub with department
await base44.entities.TeamHub.create({
team_id: userTeam.id,
hub_name: data.hub,
address: "",
is_active: true,
departments: data.department ? [{ department_name: data.department, cost_center: "" }] : []
});
queryClient.invalidateQueries({ queryKey: ['team-hubs-main', userTeam?.id] });
} else if (existingHub && data.department) {
// Add department to existing hub if it doesn't exist
const hubDepartments = existingHub.departments || [];
const departmentExists = hubDepartments.some(d => d.department_name === data.department);
if (!departmentExists) {
await base44.entities.TeamHub.update(existingHub.id, {
departments: [...hubDepartments, { department_name: data.department, cost_center: "" }]
});
queryClient.invalidateQueries({ queryKey: ['team-hubs-main', userTeam?.id] });
}
}
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,
hub: data.hub || "",
department: data.department || "",
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 || "KROW",
to: data.email,
subject: `🚀 Welcome to KROW! You've been invited to ${data.hub || userTeam.team_name}`,
body: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
<div style="text-align: center; padding: 40px 20px; background: linear-gradient(135deg, #0A39DF 0%, #1C323E 100%); border-radius: 16px; box-shadow: 0 8px 32px rgba(0,0,0,0.3);">
<div style="font-size: 64px; margin-bottom: 16px;">🎉</div>
<h1 style="color: white; margin: 0; font-size: 32px; font-weight: 800;">You're Invited!</h1>
<p style="color: #93c5fd; margin-top: 12px; font-size: 18px; font-weight: 600;">Join ${data.hub ? `the ${data.hub} hub` : userTeam.team_name || 'our team'}</p>
</div>
<div style="padding: 40px; background: white; border-radius: 16px; margin-top: 20px; box-shadow: 0 4px 16px rgba(0,0,0,0.1);">
<p style="color: #1C323E; font-size: 18px; line-height: 1.6; font-weight: 600;">
Hi ${data.full_name || 'there'} 👋
</p>
<p style="color: #475569; font-size: 16px; line-height: 1.8; margin-top: 20px;">
Great news! <strong>${user?.full_name || user?.email}</strong> has invited you to join <strong>${data.hub || userTeam.team_name || 'the team'}</strong> as a <strong style="color: #0A39DF;">${data.role}</strong>.
</p>
<div style="background: linear-gradient(135deg, #e0f2fe 0%, #ddd6fe 100%); padding: 24px; border-radius: 12px; margin: 32px 0; border-left: 6px solid #0A39DF;">
<h3 style="color: #1C323E; margin: 0 0 12px 0; font-size: 18px; font-weight: 700;">🚀 Why KROW?</h3>
<ul style="color: #475569; margin: 0; padding-left: 20px; line-height: 2; font-size: 15px;">
<li><strong>Seamless Operations:</strong> Manage your workforce effortlessly</li>
<li><strong>Smart Scheduling:</strong> AI-powered shift assignments</li>
<li><strong>Real-Time Updates:</strong> Stay connected with your team</li>
<li><strong>Simplified Workflow:</strong> Everything you need in one place</li>
</ul>
</div>
<div style="text-align: center; margin: 40px 0;">
<a href="${registerUrl}"
style="display: inline-block; padding: 20px 48px; background: linear-gradient(135deg, #0A39DF 0%, #1C323E 100%);
color: white; text-decoration: none; border-radius: 12px; font-weight: 800; font-size: 18px;
box-shadow: 0 8px 24px rgba(10, 57, 223, 0.4); transition: all 0.3s;">
🎯 Get Started Now →
</a>
</div>
<div style="background: #f8fafc; padding: 20px; border-radius: 12px; margin: 32px 0; border: 2px solid #e2e8f0;">
<h3 style="color: #1C323E; margin: 0 0 12px 0; font-size: 16px; font-weight: 700;">✅ Quick Setup (3 Steps):</h3>
<ol style="color: #475569; margin: 0; padding-left: 20px; line-height: 2; font-size: 15px;">
<li><strong>Click</strong> the button above to register</li>
<li><strong>Create</strong> your account with <span style="color: #0A39DF; font-weight: 600;">${data.email}</span></li>
<li><strong>Start</strong> managing your operations smoothly!</li>
</ol>
</div>
<div style="background: linear-gradient(135deg, #fef3c7 0%, #fed7aa 100%); padding: 20px; border-radius: 12px; margin-top: 32px; border: 2px solid #fbbf24;">
<p style="color: #92400e; font-size: 15px; margin: 0; font-weight: 600;">
⏰ <strong>Time-Sensitive:</strong> This invitation expires in 7 days. Don't miss out!
</p>
</div>
<div style="margin-top: 40px; padding-top: 24px; border-top: 2px solid #e2e8f0; text-align: center;">
<p style="color: #94a3b8; font-size: 13px; margin: 8px 0;">
Your invite code: <strong style="color: #475569; font-family: monospace; background: #f1f5f9; padding: 4px 8px; border-radius: 4px;">${inviteCode}</strong>
</p>
<p style="color: #94a3b8; font-size: 13px; margin: 8px 0;">
Questions? Reach out to <a href="mailto:${user?.email || 'support@krow.com'}" style="color: #0A39DF; font-weight: 600;">${user?.email || 'support'}</a>
</p>
</div>
</div>
<div style="text-align: center; padding: 20px; color: #cbd5e1; font-size: 12px;">
<p style="margin: 0;">Powered by <strong>KROW</strong> - Workforce Control Tower</p>
</div>
</div>
`
});
return { invite };
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['team-members', userTeam?.id] });
queryClient.invalidateQueries({ queryKey: ['team-invites', userTeam?.id] });
setShowInviteMemberDialog(false);
setPreSelectedHub(null);
setInviteData({
email: "",
full_name: "",
role: "member",
hub: "",
department: "",
});
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 updateTeamMutation = useMutation({
mutationFn: ({ id, data }) => base44.entities.Team.update(id, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['user-team', user?.id, userRole] });
toast({
title: "✅ Team Updated",
description: "Team updated successfully",
});
},
});
const addToFavorites = (staff) => {
const favoriteStaff = userTeam.favorite_staff || [];
const newFavorite = {
staff_id: staff.id,
staff_name: staff.employee_name,
position: staff.position,
added_date: new Date().toISOString()
};
updateTeamMutation.mutate({
id: userTeam.id,
data: {
favorite_staff: [...favoriteStaff, newFavorite],
favorite_staff_count: favoriteStaff.length + 1
}
});
setShowAddFavoriteDialog(false);
};
const removeFromFavorites = (staffId) => {
const favoriteStaff = (userTeam.favorite_staff || []).filter(f => f.staff_id !== staffId);
updateTeamMutation.mutate({
id: userTeam.id,
data: {
favorite_staff: favoriteStaff,
favorite_staff_count: favoriteStaff.length
}
});
};
const addToBlocked = (staff) => {
const blockedStaff = userTeam.blocked_staff || [];
const newBlocked = {
staff_id: staff.id,
staff_name: staff.employee_name,
reason: blockReason,
blocked_date: new Date().toISOString()
};
updateTeamMutation.mutate({
id: userTeam.id,
data: {
blocked_staff: [...blockedStaff, newBlocked],
blocked_staff_count: blockedStaff.length + 1
}
});
setShowAddBlockedDialog(false);
setBlockReason("");
};
const removeFromBlocked = (staffId) => {
const blockedStaff = (userTeam.blocked_staff || []).filter(b => b.staff_id !== staffId);
updateTeamMutation.mutate({
id: userTeam.id,
data: {
blocked_staff: blockedStaff,
blocked_staff_count: blockedStaff.length
}
});
};
// Load Google Maps script
useEffect(() => {
if (window.google?.maps?.places) {
setIsGoogleMapsLoaded(true);
return;
}
const script = document.createElement('script');
script.src = `https://maps.googleapis.com/maps/api/js?key=AIzaSyBkP7xH4NvR6C6vZ8Y3J7qX2QW8Z9vN3Zc&libraries=places`;
script.async = true;
script.onload = () => setIsGoogleMapsLoaded(true);
document.head.appendChild(script);
}, []);
// Initialize autocomplete
useEffect(() => {
if (isGoogleMapsLoaded && addressInputRef.current && showAddHubDialog && !autocompleteRef.current) {
autocompleteRef.current = new window.google.maps.places.Autocomplete(addressInputRef.current, {
types: ['address'],
componentRestrictions: { country: 'us' }
});
autocompleteRef.current.addListener('place_changed', () => {
const place = autocompleteRef.current.getPlace();
if (place.formatted_address) {
setNewHub({ ...newHub, address: place.formatted_address });
}
});
}
}, [isGoogleMapsLoaded, showAddHubDialog]);
const createHubMutation = useMutation({
mutationFn: (hubData) => base44.entities.TeamHub.create({
...hubData,
team_id: userTeam.id,
is_active: true
}),
onSuccess: (createdHub) => {
queryClient.invalidateQueries({ queryKey: ['team-hubs-main', userTeam?.id] });
setShowAddHubDialog(false);
const hubName = newHub.hub_name;
setNewHub({
hub_name: "",
address: "",
manager_name: "",
manager_position: "",
manager_email: ""
});
autocompleteRef.current = null;
// Show success with invite action
toast({
title: "✅ Hub Created Successfully!",
description: (
<div className="flex items-center gap-2 mt-2">
<span>Ready to invite members to {hubName}?</span>
<Button
size="sm"
className="bg-[#0A39DF] hover:bg-blue-700 h-7 px-3"
onClick={() => {
setPreSelectedHub(hubName);
setInviteData({ ...inviteData, hub: hubName });
setShowInviteMemberDialog(true);
}}
>
Invite Now
</Button>
</div>
),
});
},
});
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>
)}
<div className="flex gap-2 flex-wrap">
{member.department && (
<Badge variant="outline" className="text-xs bg-blue-50 text-blue-700 border-blue-200">
{member.department}
</Badge>
)}
{member.hub && (
<Badge variant="outline" className="text-xs bg-purple-50 text-purple-700 border-purple-200 flex items-center gap-1">
<MapPin className="w-3 h-3" />
{member.hub}
</Badge>
)}
</div>
</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={() => setShowAddHubDialog(true)}
className="hover:bg-slate-50 hover:border-[#0A39DF] hover:text-[#0A39DF] transition-all"
>
<Plus className="w-4 h-4 mr-2" />
Create Hub
</Button>
<Button
onClick={() => createTestInviteMutation.mutate()}
disabled={createTestInviteMutation.isPending || !userTeam?.id}
className="bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white shadow-lg font-bold"
size="lg"
>
{createTestInviteMutation.isPending ? (
<>
<Loader2 className="w-5 h-5 mr-2 animate-spin" />
Loading...
</>
) : (
<>
🎯 Get Started Now
</>
)}
</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, Invitations, Hubs, Favorites, Blocked */}
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid w-full grid-cols-6 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>
<TabsTrigger value="hubs" className="data-[state=active]:bg-white data-[state=active]:shadow-sm flex items-center gap-2">
<MapPin className="w-4 h-4" />
Hubs ({teamHubs.length})
</TabsTrigger>
<TabsTrigger value="favorites" className="data-[state=active]:bg-white data-[state=active]:shadow-sm flex items-center gap-2">
<Star className="w-4 h-4" />
Favorites ({userTeam?.favorite_staff_count || 0})
</TabsTrigger>
<TabsTrigger value="blocked" className="data-[state=active]:bg-white data-[state=active]:shadow-sm flex items-center gap-2">
<UserX className="w-4 h-4" />
Blocked ({userTeam?.blocked_staff_count || 0})
</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="bg-white rounded-xl border border-slate-200 overflow-hidden">
<table className="w-full">
<thead className="bg-slate-50 border-b border-slate-200">
<tr>
<th className="text-left px-4 py-3 text-xs font-semibold text-slate-600 uppercase tracking-wider">#</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-slate-600 uppercase tracking-wider">Name</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-slate-600 uppercase tracking-wider">Title</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-slate-600 uppercase tracking-wider">Role</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-slate-600 uppercase tracking-wider">Department</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-slate-600 uppercase tracking-wider">Hub</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-slate-600 uppercase tracking-wider">Hub Address</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-slate-600 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{filteredMembers(activeMembers).map((member, index) => {
const memberHub = teamHubs.find(h => h.hub_name === member.hub);
return (
<tr key={member.id} className="hover:bg-slate-50 transition-colors">
<td className="px-4 py-4 text-sm text-slate-600">{index + 1}</td>
<td className="px-4 py-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-br from-[#0A39DF] to-[#1C323E] rounded-lg flex items-center justify-center text-white font-bold text-sm">
{member.member_name?.split(' ').map(n => n[0]).join('') || '?'}
</div>
<div>
<p className="font-semibold text-slate-900">{member.member_name}</p>
<p className="text-xs text-slate-500">{member.email}</p>
</div>
</div>
</td>
<td className="px-4 py-4 text-sm text-slate-700">{member.title || '-'}</td>
<td className="px-4 py-4">
<Badge variant="outline" className="capitalize text-xs">
{member.role}
</Badge>
</td>
<td className="px-4 py-4 text-sm text-slate-700">{member.department || 'No Department'}</td>
<td className="px-4 py-4 text-sm text-slate-700">{member.hub || 'No Hub'}</td>
<td className="px-4 py-4 text-sm text-slate-600">{memberHub?.address || 'No Address'}</td>
<td className="px-4 py-4">
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="icon"
onClick={() => handleEditMember(member)}
className="h-8 w-8 hover:bg-blue-50 hover:text-[#0A39DF]"
>
<Edit className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => handleDeactivateMember(member)}
className="h-8 w-8 hover:bg-red-50 hover:text-red-600"
>
<UserX className="w-4 h-4" />
</Button>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</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="bg-white rounded-xl border border-slate-200 overflow-hidden opacity-70">
<table className="w-full">
<thead className="bg-slate-50 border-b border-slate-200">
<tr>
<th className="text-left px-4 py-3 text-xs font-semibold text-slate-600 uppercase tracking-wider">#</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-slate-600 uppercase tracking-wider">Name</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-slate-600 uppercase tracking-wider">Title</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-slate-600 uppercase tracking-wider">Role</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-slate-600 uppercase tracking-wider">Department</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-slate-600 uppercase tracking-wider">Hub</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-slate-600 uppercase tracking-wider">Hub Address</th>
<th className="text-left px-4 py-3 text-xs font-semibold text-slate-600 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{filteredMembers(deactivatedMembers).map((member, index) => {
const memberHub = teamHubs.find(h => h.hub_name === member.hub);
return (
<tr key={member.id} className="hover:bg-slate-50 transition-colors">
<td className="px-4 py-4 text-sm text-slate-600">{index + 1}</td>
<td className="px-4 py-4">
<div className="flex items-center gap-3">
<div className="relative w-10 h-10 bg-gradient-to-br from-slate-400 to-slate-600 rounded-lg flex items-center justify-center text-white font-bold text-sm">
{member.member_name?.split(' ').map(n => n[0]).join('') || '?'}
<div className="absolute -top-1 -right-1 w-5 h-5 bg-red-500 rounded-full flex items-center justify-center ring-2 ring-white">
<UserX className="w-2.5 h-2.5 text-white" />
</div>
</div>
<div>
<p className="font-semibold text-slate-900">{member.member_name}</p>
<p className="text-xs text-slate-500">{member.email}</p>
</div>
</div>
</td>
<td className="px-4 py-4 text-sm text-slate-700">{member.title || '-'}</td>
<td className="px-4 py-4">
<Badge variant="outline" className="capitalize text-xs">
{member.role}
</Badge>
</td>
<td className="px-4 py-4 text-sm text-slate-700">{member.department || 'No Department'}</td>
<td className="px-4 py-4 text-sm text-slate-700">{member.hub || 'No Hub'}</td>
<td className="px-4 py-4 text-sm text-slate-600">{memberHub?.address || 'No Address'}</td>
<td className="px-4 py-4">
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="icon"
onClick={() => handleEditMember(member)}
className="h-8 w-8 hover:bg-blue-50 hover:text-[#0A39DF]"
>
<Edit className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => handleActivateMember(member)}
className="h-8 w-8 hover:bg-green-50 hover:text-green-600"
>
<UserCheck className="w-4 h-4" />
</Button>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</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>
{/* Hubs Tab */}
<TabsContent value="hubs">
<div className="space-y-6">
{teamHubs.length > 0 ? (
viewMode === "grid" ? (
<div className="space-y-6">
{teamHubs.map((hub) => (
<div key={hub.id} className="bg-white rounded-xl border-2 border-slate-200 overflow-hidden hover:shadow-xl transition-all duration-300">
{/* Hub Header */}
<div className="bg-gradient-to-r from-slate-50 via-blue-50 to-slate-50 border-b-2 border-slate-200 p-6">
<div className="flex items-start justify-between mb-4">
<div className="flex items-start gap-4 flex-1">
<div className="w-16 h-16 bg-gradient-to-br from-[#0A39DF] to-[#1C323E] rounded-2xl flex items-center justify-center shadow-lg">
<Building2 className="w-8 h-8 text-white" />
</div>
<div className="flex-1">
<h3 className="text-2xl font-bold text-[#1C323E] mb-2">{hub.hub_name}</h3>
{hub.address && (
<div className="flex items-start gap-2 text-slate-600 mb-3">
<MapPin className="w-4 h-4 mt-1 flex-shrink-0" />
<p className="text-sm">{hub.address}</p>
</div>
)}
{hub.manager_name && (
<div className="flex items-center gap-3 text-sm">
<div className="flex items-center gap-2 bg-white px-3 py-1.5 rounded-lg border border-slate-200">
<UserCheck className="w-4 h-4 text-[#0A39DF]" />
<span className="font-medium text-slate-700">{hub.manager_name}</span>
</div>
{hub.manager_email && (
<a href={`mailto:${hub.manager_email}`} className="flex items-center gap-2 text-slate-600 hover:text-[#0A39DF] transition-colors">
<Mail className="w-4 h-4" />
{hub.manager_email}
</a>
)}
</div>
)}
</div>
</div>
<div className="flex gap-2">
<Button
size="sm"
className="bg-[#0A39DF] hover:bg-blue-700 text-white"
onClick={() => {
setPreSelectedHub(hub.hub_name);
setInviteData({ ...inviteData, hub: hub.hub_name });
setShowInviteMemberDialog(true);
}}
>
<UserPlus className="w-4 h-4 mr-2" />
Invite Member
</Button>
</div>
</div>
{/* Quick Stats */}
<div className="flex items-center gap-6 mt-4">
<div className="flex items-center gap-2 text-sm">
<div className="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center">
<Users className="w-4 h-4 text-blue-600" />
</div>
<div>
<p className="text-xs text-slate-500 font-medium">Team Members</p>
<p className="text-lg font-bold text-slate-900">{activeMembers.filter(m => m.hub === hub.hub_name).length}</p>
</div>
</div>
<div className="flex items-center gap-2 text-sm">
<div className="w-8 h-8 bg-purple-100 rounded-lg flex items-center justify-center">
<Building2 className="w-4 h-4 text-purple-600" />
</div>
<div>
<p className="text-xs text-slate-500 font-medium">Departments</p>
<p className="text-lg font-bold text-slate-900">{hub.departments?.length || 0}</p>
</div>
</div>
</div>
</div>
{/* Departments Section */}
<div className="p-6">
<div className="flex items-center justify-between mb-4">
<h4 className="text-lg font-bold text-[#1C323E] flex items-center gap-2">
<Building2 className="w-5 h-5 text-[#0A39DF]" />
Departments
</h4>
<Button
variant="outline"
size="sm"
onClick={() => {
setSelectedHubForDept(hub);
setShowAddHubDepartmentDialog(true);
}}
>
<Plus className="w-4 h-4 mr-2" />
Create Department
</Button>
</div>
{hub.departments && hub.departments.length > 0 ? (
<div className="space-y-4">
{hub.departments.map((dept, idx) => (
<div key={idx} className="bg-slate-50 rounded-xl border border-slate-200 overflow-hidden hover:border-[#0A39DF] transition-all">
<div className="p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-br from-purple-500 to-blue-600 rounded-lg flex items-center justify-center">
<Building2 className="w-5 h-5 text-white" />
</div>
<div>
<h5 className="font-bold text-slate-900">{dept.department_name}</h5>
{dept.cost_center && (
<p className="text-xs text-slate-500 font-mono mt-0.5">Cost Center: {dept.cost_center}</p>
)}
</div>
</div>
{dept.manager_name && (
<Badge className="bg-blue-100 text-blue-700 border-blue-200">
{dept.manager_name}
</Badge>
)}
</div>
{/* Team members in this department */}
{activeMembers.filter(m => m.hub === hub.hub_name && m.department === dept.department_name).length > 0 && (
<div className="mt-3 pt-3 border-t border-slate-200">
<p className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2">
Team ({activeMembers.filter(m => m.hub === hub.hub_name && m.department === dept.department_name).length})
</p>
<div className="flex flex-wrap gap-2">
{activeMembers.filter(m => m.hub === hub.hub_name && m.department === dept.department_name).map((member) => (
<div key={member.id} className="flex items-center gap-2 bg-white px-3 py-2 rounded-lg border border-slate-200 hover:border-[#0A39DF] transition-all group cursor-pointer" onClick={() => handleEditMember(member)}>
<div className="w-7 h-7 bg-gradient-to-br from-[#0A39DF] to-[#1C323E] rounded-lg flex items-center justify-center text-white font-bold text-xs">
{member.member_name?.split(' ').map(n => n[0]).join('') || '?'}
</div>
<div>
<p className="text-sm font-semibold text-slate-900 group-hover:text-[#0A39DF] transition-colors">{member.member_name}</p>
<p className="text-xs text-slate-500">{member.title || member.role}</p>
</div>
</div>
))}
</div>
</div>
)}
</div>
</div>
))}
</div>
) : (
<div className="text-center py-12 bg-slate-50 rounded-xl border-2 border-dashed border-slate-200">
<Building2 className="w-12 h-12 mx-auto text-slate-300 mb-3" />
<p className="text-sm text-slate-500 font-medium">No departments yet</p>
<Button
variant="outline"
size="sm"
className="mt-4"
onClick={() => {
setSelectedHubForDept(hub);
setShowAddHubDepartmentDialog(true);
}}
>
<Plus className="w-4 h-4 mr-2" />
Create First Department
</Button>
</div>
)}
</div>
</div>
))}
{/* Add New Hub Button */}
<Button
onClick={() => setShowAddHubDialog(true)}
size="lg"
className="w-full bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white shadow-lg h-16"
>
<Plus className="w-5 h-5 mr-2" />
Create New Hub
</Button>
</div>
) : (
<div className="space-y-4">
<Table>
<TableHeader>
<TableRow>
<TableHead>Hub Name</TableHead>
<TableHead>Address</TableHead>
<TableHead>Manager</TableHead>
<TableHead>Departments</TableHead>
<TableHead>Members</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{teamHubs.map((hub) => (
<TableRow key={hub.id} className="hover:bg-blue-50">
<TableCell>
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-br from-[#0A39DF] to-[#1C323E] rounded-lg flex items-center justify-center">
<Building2 className="w-5 h-5 text-white" />
</div>
<span className="font-bold text-[#1C323E]">{hub.hub_name}</span>
</div>
</TableCell>
<TableCell>
<div className="flex items-start gap-2">
<MapPin className="w-4 h-4 text-slate-400 mt-0.5 flex-shrink-0" />
<span className="text-sm text-slate-600">{hub.address || '—'}</span>
</div>
</TableCell>
<TableCell>
<div>
<p className="font-medium text-slate-900">{hub.manager_name || '—'}</p>
{hub.manager_email && (
<p className="text-xs text-slate-500">{hub.manager_email}</p>
)}
</div>
</TableCell>
<TableCell>
<Badge className="bg-purple-100 text-purple-700 border-purple-200">
{hub.departments?.length || 0} depts
</Badge>
</TableCell>
<TableCell>
<Badge className="bg-blue-100 text-blue-700 border-blue-200">
{activeMembers.filter(m => m.hub === hub.hub_name).length} members
</Badge>
</TableCell>
<TableCell className="text-right">
<div className="flex items-center justify-end gap-2">
<Button
variant="outline"
size="sm"
onClick={() => {
setSelectedHubForDept(hub);
setShowAddHubDepartmentDialog(true);
}}
>
<Plus className="w-4 h-4 mr-1" />
Dept
</Button>
<Button
size="sm"
className="bg-[#0A39DF] hover:bg-blue-700"
onClick={() => {
setPreSelectedHub(hub.hub_name);
setInviteData({ ...inviteData, hub: hub.hub_name });
setShowInviteMemberDialog(true);
}}
>
<UserPlus className="w-4 h-4 mr-1" />
Invite
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<Button
onClick={() => setShowAddHubDialog(true)}
size="lg"
className="w-full bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white shadow-lg h-12"
>
<Plus className="w-5 h-5 mr-2" />
Create New Hub
</Button>
</div>
)
) : (
<div className="text-center py-20 bg-gradient-to-br from-white to-blue-50 rounded-2xl border-2 border-dashed border-blue-200">
<div className="w-24 h-24 mx-auto mb-6 rounded-full bg-gradient-to-br from-blue-100 to-indigo-100 flex items-center justify-center">
<Building2 className="w-12 h-12 text-blue-600" />
</div>
<h3 className="text-2xl font-bold text-slate-800 mb-3">No Hubs Yet</h3>
<p className="text-slate-600 mb-8 max-w-md mx-auto">
Create your first hub location to organize your team by physical locations and departments
</p>
<Button size="lg" className="bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white shadow-lg" onClick={() => setShowAddHubDialog(true)}>
<MapPin className="w-5 h-5 mr-2" />
Create First Hub
</Button>
</div>
)}
</div>
</TabsContent>
{/* Favorites Tab */}
<TabsContent value="favorites">
<div className="space-y-6">
{/* Header Section */}
<div className="bg-gradient-to-r from-amber-50 via-yellow-50 to-amber-50 rounded-2xl border-2 border-amber-200 p-6">
<div className="flex items-start justify-between mb-4">
<div>
<h3 className="text-2xl font-bold text-[#1C323E] flex items-center gap-3 mb-2">
<div className="w-12 h-12 bg-gradient-to-br from-amber-400 to-orange-500 rounded-xl flex items-center justify-center shadow-lg">
<Star className="w-6 h-6 text-white fill-white" />
</div>
Preferred Staff
</h3>
<p className="text-slate-600 text-sm">Your go-to professionals for high-priority assignments</p>
</div>
<div className="flex items-center gap-3">
<div className="text-right">
<p className="text-3xl font-bold text-amber-600">{userTeam?.favorite_staff_count || 0}</p>
<p className="text-xs text-slate-500 font-medium">Favorites</p>
</div>
<Button onClick={() => setShowAddFavoriteDialog(true)} className="bg-gradient-to-r from-amber-500 to-orange-600 hover:from-amber-600 hover:to-orange-700 text-white shadow-lg">
<Star className="w-4 h-4 mr-2" />
Add Favorite
</Button>
</div>
</div>
{/* Search */}
<div className="relative">
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 w-5 h-5 text-amber-500" />
<Input
placeholder="Search your favorite staff by name or role..."
value={favoriteSearch}
onChange={(e) => setFavoriteSearch(e.target.value)}
className="pl-12 h-12 bg-white border-2 border-amber-200 focus:border-amber-400 text-base"
/>
</div>
</div>
{userTeam?.favorite_staff && userTeam.favorite_staff.length > 0 ? (
viewMode === "grid" ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{userTeam.favorite_staff.filter(f =>
!favoriteSearch ||
f.staff_name?.toLowerCase().includes(favoriteSearch.toLowerCase()) ||
f.position?.toLowerCase().includes(favoriteSearch.toLowerCase())
).map((fav) => (
<div key={fav.staff_id} className="group bg-gradient-to-br from-white to-amber-50 rounded-2xl border-2 border-amber-200 hover:border-amber-400 hover:shadow-2xl transition-all duration-300 overflow-hidden">
<div className="p-6">
<div className="flex items-start justify-between mb-4">
<div className="flex items-start gap-3 flex-1">
<div className="relative">
<div className="w-16 h-16 bg-gradient-to-br from-amber-400 to-orange-500 rounded-2xl flex items-center justify-center text-white font-bold text-xl shadow-lg">
{fav.staff_name?.charAt(0)}
</div>
<div className="absolute -top-1 -right-1 w-7 h-7 bg-amber-500 rounded-full flex items-center justify-center shadow-md">
<Star className="w-4 h-4 text-white fill-white" />
</div>
</div>
<div className="flex-1 min-w-0">
<h4 className="font-bold text-lg text-slate-900 truncate group-hover:text-amber-600 transition-colors">{fav.staff_name}</h4>
<p className="text-sm text-slate-600 font-medium">{fav.position}</p>
<p className="text-xs text-amber-600 mt-1 bg-amber-100 px-2 py-1 rounded-full inline-block">
Added {new Date(fav.added_date).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}
</p>
</div>
</div>
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => removeFromFavorites(fav.staff_id)}
className="flex-1 border-2 border-amber-300 text-amber-700 hover:bg-amber-100 hover:border-amber-400 font-semibold"
>
<Star className="w-4 h-4 mr-2" />
Remove
</Button>
</div>
</div>
</div>
))}
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Staff Member</TableHead>
<TableHead>Position</TableHead>
<TableHead>Added Date</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{userTeam.favorite_staff.filter(f =>
!favoriteSearch ||
f.staff_name?.toLowerCase().includes(favoriteSearch.toLowerCase()) ||
f.position?.toLowerCase().includes(favoriteSearch.toLowerCase())
).map((fav) => (
<TableRow key={fav.staff_id} className="hover:bg-amber-50">
<TableCell>
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-br from-amber-400 to-orange-500 rounded-lg flex items-center justify-center text-white font-bold">
{fav.staff_name?.charAt(0)}
</div>
<span className="font-semibold">{fav.staff_name}</span>
<Star className="w-4 h-4 text-amber-500 fill-amber-500" />
</div>
</TableCell>
<TableCell>{fav.position}</TableCell>
<TableCell>
<span className="text-xs text-slate-600">
{new Date(fav.added_date).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}
</span>
</TableCell>
<TableCell className="text-right">
<Button
variant="outline"
size="sm"
onClick={() => removeFromFavorites(fav.staff_id)}
className="border-amber-300 text-amber-700 hover:bg-amber-100"
>
Remove
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)
) : (
<div className="text-center py-24 bg-gradient-to-br from-white to-amber-50 rounded-2xl border-2 border-dashed border-amber-200">
<div className="w-24 h-24 mx-auto mb-6 rounded-full bg-gradient-to-br from-amber-100 to-orange-100 flex items-center justify-center">
<Star className="w-12 h-12 text-amber-500" />
</div>
<h3 className="text-2xl font-bold text-slate-800 mb-3">No Favorites Yet</h3>
<p className="text-slate-600 mb-8 max-w-md mx-auto">
Build your dream team by marking your most reliable and skilled staff members as favorites
</p>
<Button onClick={() => setShowAddFavoriteDialog(true)} size="lg" className="bg-gradient-to-r from-amber-500 to-orange-600 hover:from-amber-600 hover:to-orange-700 text-white shadow-lg">
<Star className="w-5 h-5 mr-2" />
Add Your First Favorite
</Button>
</div>
)}
</div>
</TabsContent>
{/* Blocked Staff Tab */}
<TabsContent value="blocked">
<div className="space-y-6">
{/* Header Section */}
<div className="bg-gradient-to-r from-red-50 via-orange-50 to-red-50 rounded-2xl border-2 border-red-200 p-6">
<div className="flex items-start justify-between mb-4">
<div>
<h3 className="text-2xl font-bold text-[#1C323E] flex items-center gap-3 mb-2">
<div className="w-12 h-12 bg-gradient-to-br from-red-500 to-red-700 rounded-xl flex items-center justify-center shadow-lg">
<UserX className="w-6 h-6 text-white" />
</div>
Blocked Staff
</h3>
<p className="text-slate-600 text-sm">Staff members excluded from future assignments</p>
</div>
<div className="flex items-center gap-3">
<div className="text-right">
<p className="text-3xl font-bold text-red-600">{userTeam?.blocked_staff_count || 0}</p>
<p className="text-xs text-slate-500 font-medium">Blocked</p>
</div>
<Button onClick={() => setShowAddBlockedDialog(true)} variant="outline" className="border-2 border-red-300 text-red-700 hover:bg-red-50 hover:border-red-400 font-semibold shadow-sm">
<UserX className="w-4 h-4 mr-2" />
Block Staff
</Button>
</div>
</div>
{/* Search */}
<div className="relative">
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 w-5 h-5 text-red-500" />
<Input
placeholder="Search blocked staff by name..."
value={blockedSearch}
onChange={(e) => setBlockedSearch(e.target.value)}
className="pl-12 h-12 bg-white border-2 border-red-200 focus:border-red-400 text-base"
/>
</div>
</div>
{userTeam?.blocked_staff && userTeam.blocked_staff.length > 0 ? (
viewMode === "grid" ? (
<div className="space-y-4">
{userTeam.blocked_staff.filter(b =>
!blockedSearch ||
b.staff_name?.toLowerCase().includes(blockedSearch.toLowerCase())
).map((blocked) => (
<div key={blocked.staff_id} className="group bg-gradient-to-br from-white to-red-50 rounded-2xl border-2 border-red-200 hover:border-red-400 hover:shadow-xl transition-all duration-300 overflow-hidden">
<div className="p-6">
<div className="flex items-start justify-between">
<div className="flex items-start gap-4 flex-1">
<div className="relative">
<div className="w-16 h-16 bg-gradient-to-br from-red-500 to-red-700 rounded-2xl flex items-center justify-center text-white font-bold text-xl shadow-lg">
{blocked.staff_name?.charAt(0)}
</div>
<div className="absolute -top-1 -right-1 w-7 h-7 bg-red-600 rounded-full flex items-center justify-center shadow-md ring-2 ring-white">
<UserX className="w-4 h-4 text-white" />
</div>
</div>
<div className="flex-1 min-w-0">
<h4 className="font-bold text-lg text-slate-900 mb-2">{blocked.staff_name}</h4>
<div className="bg-red-100 border-l-4 border-red-500 p-3 rounded-lg mb-2">
<p className="text-xs font-semibold text-red-700 uppercase tracking-wide mb-1">Block Reason</p>
<p className="text-sm text-red-900 font-medium">{blocked.reason || 'No reason provided'}</p>
</div>
<div className="flex items-center gap-2 text-xs text-slate-500">
<div className="flex items-center gap-1">
<div className="w-1.5 h-1.5 bg-red-500 rounded-full"></div>
Blocked on {new Date(blocked.blocked_date).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' })}
</div>
</div>
</div>
</div>
<Button
variant="outline"
size="sm"
onClick={() => removeFromBlocked(blocked.staff_id)}
className="border-2 border-green-300 text-green-700 hover:bg-green-50 hover:border-green-400 font-semibold"
>
<UserCheck className="w-4 h-4 mr-2" />
Unblock
</Button>
</div>
</div>
</div>
))}
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Staff Member</TableHead>
<TableHead>Reason</TableHead>
<TableHead>Blocked Date</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{userTeam.blocked_staff.filter(b =>
!blockedSearch ||
b.staff_name?.toLowerCase().includes(blockedSearch.toLowerCase())
).map((blocked) => (
<TableRow key={blocked.staff_id} className="hover:bg-red-50">
<TableCell>
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-br from-red-500 to-red-700 rounded-lg flex items-center justify-center text-white font-bold">
{blocked.staff_name?.charAt(0)}
</div>
<span className="font-semibold">{blocked.staff_name}</span>
<UserX className="w-4 h-4 text-red-600" />
</div>
</TableCell>
<TableCell>
<span className="text-sm text-red-900">{blocked.reason || 'No reason provided'}</span>
</TableCell>
<TableCell>
<span className="text-xs text-slate-600">
{new Date(blocked.blocked_date).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' })}
</span>
</TableCell>
<TableCell className="text-right">
<Button
variant="outline"
size="sm"
onClick={() => removeFromBlocked(blocked.staff_id)}
className="border-green-300 text-green-700 hover:bg-green-50"
>
<UserCheck className="w-4 h-4 mr-2" />
Unblock
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)
) : (
<div className="text-center py-24 bg-gradient-to-br from-white to-slate-50 rounded-2xl border-2 border-dashed border-slate-200">
<div className="w-24 h-24 mx-auto mb-6 rounded-full bg-gradient-to-br from-green-100 to-emerald-100 flex items-center justify-center">
<UserCheck className="w-12 h-12 text-green-600" />
</div>
<h3 className="text-2xl font-bold text-slate-800 mb-3">No Blocked Staff</h3>
<p className="text-slate-600 mb-2 max-w-md mx-auto">
All staff members are currently eligible for assignments
</p>
<p className="text-sm text-slate-500">
Block staff members who should not be assigned to your events
</p>
</div>
)}
</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>
<div className="grid grid-cols-2 gap-4">
<div>
<Label>Hub Location</Label>
<Input
value={inviteData.hub}
onChange={(e) => setInviteData({ ...inviteData, hub: e.target.value })}
placeholder="e.g., BVG300"
list="existing-hubs"
/>
<datalist id="existing-hubs">
{teamHubs.map((hub) => {
const isRecent = new Date() - new Date(hub.created_date) < 24 * 60 * 60 * 1000;
return (
<option key={hub.id} value={hub.hub_name}>
{isRecent ? '✨ ' : ''}{hub.hub_name}
</option>
);
})}
</datalist>
<p className="text-xs text-slate-500 mt-1">
{preSelectedHub && <span className="text-[#0A39DF] font-semibold"> Pre-selected from hub creation </span>}
{teamHubs.find(h => h.hub_name === inviteData.hub) ? '✓ Existing hub' : inviteData.hub ? '+ Will create new hub' : 'Type to search or create'}
</p>
</div>
<div>
<Label>Department</Label>
<Input
value={inviteData.department}
onChange={(e) => setInviteData({ ...inviteData, department: e.target.value })}
placeholder="e.g., Catering FOH"
list="existing-departments"
/>
<datalist id="existing-departments">
{uniqueDepartments.map((dept) => (
<option key={dept} value={dept} />
))}
</datalist>
</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>
<Label>Hub Location</Label>
<Input
value={editingMember.hub || ""}
onChange={(e) => setEditingMember({ ...editingMember, hub: e.target.value })}
placeholder="e.g., BVG300"
list="existing-hubs-edit"
/>
<datalist id="existing-hubs-edit">
{teamHubs.map((hub) => (
<option key={hub.id} value={hub.hub_name} />
))}
</datalist>
</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>
{/* Add Hub Dialog */}
<Dialog open={showAddHubDialog} onOpenChange={setShowAddHubDialog}>
<DialogContent className="max-w-3xl">
<DialogHeader className="pb-6 border-b">
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-xl flex items-center justify-center">
<Building2 className="w-6 h-6 text-white" />
</div>
<div>
<DialogTitle className="text-2xl">Create New Hub</DialogTitle>
<DialogDescription className="text-base">Add a new location hub for your team members</DialogDescription>
</div>
</div>
</DialogHeader>
<div className="space-y-6 py-4">
<div className="bg-blue-50 border-l-4 border-blue-500 p-4 rounded-r-lg">
<p className="text-sm text-blue-900">
<strong>Quick Tip:</strong> When inviting members, they can type this hub name (e.g., "BVG300") and it will auto-suggest!
</p>
</div>
<div>
<Label className="text-base font-semibold mb-2 flex items-center gap-2">
<Building2 className="w-4 h-4 text-blue-600" />
Hub Information *
</Label>
<Input
value={newHub.hub_name}
onChange={(e) => setNewHub({ ...newHub, hub_name: e.target.value })}
placeholder="e.g., BVG300, Main Office, Downtown Location"
className="text-lg h-12 border-2"
/>
</div>
<div className="space-y-3">
<Label className="text-base font-semibold flex items-center gap-2">
<MapPin className="w-4 h-4 text-blue-600" />
Location Address
</Label>
<Input
ref={addressInputRef}
value={newHub.address}
onChange={(e) => setNewHub({ ...newHub, address: e.target.value })}
placeholder="Start typing address... (e.g., 300 Bayview Dr)"
className="h-11"
/>
</div>
<div className="space-y-4">
<Label className="text-base font-semibold flex items-center gap-2">
<UserCheck className="w-4 h-4 text-blue-600" />
Hub Manager *
</Label>
<div className="grid grid-cols-2 gap-4">
<Input
value={newHub.manager_name}
onChange={(e) => setNewHub({ ...newHub, manager_name: e.target.value })}
placeholder="Manager Name *"
className="h-11"
required
/>
<Input
value={newHub.manager_position}
onChange={(e) => setNewHub({ ...newHub, manager_position: e.target.value })}
placeholder="Position/Title *"
className="h-11"
required
/>
</div>
<Input
type="email"
value={newHub.manager_email}
onChange={(e) => setNewHub({ ...newHub, manager_email: e.target.value })}
placeholder="manager@example.com"
className="h-11"
/>
</div>
</div>
<DialogFooter className="border-t pt-6">
<Button variant="outline" onClick={() => setShowAddHubDialog(false)} size="lg">Cancel</Button>
<Button
onClick={() => createHubMutation.mutate(newHub)}
size="lg"
className="bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white shadow-lg"
disabled={!newHub.hub_name || !newHub.manager_name || !newHub.manager_position || createHubMutation.isPending}
>
{createHubMutation.isPending ? (
<>
<Loader2 className="w-5 h-5 mr-2 animate-spin" />
Creating Hub...
</>
) : (
<>
<Building2 className="w-5 h-5 mr-2" />
Create Hub
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Add Hub Department Dialog */}
<Dialog open={showAddHubDepartmentDialog} onOpenChange={setShowAddHubDepartmentDialog}>
<DialogContent className="max-w-2xl">
<DialogHeader className="pb-4 border-b">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-lg flex items-center justify-center">
<Building2 className="w-5 h-5 text-white" />
</div>
<div>
<DialogTitle className="text-xl">Add Department</DialogTitle>
<DialogDescription>to {selectedHubForDept?.hub_name}</DialogDescription>
</div>
</div>
</DialogHeader>
<div className="space-y-5 py-4">
<div>
<Label className="text-base font-semibold mb-2 block">Department Name *</Label>
<Input
value={newHubDepartment.department_name}
onChange={(e) => setNewHubDepartment({ ...newHubDepartment, department_name: e.target.value })}
placeholder="e.g., Catering FOH, Catering BOH, Operations"
className="h-11 text-base"
/>
</div>
<div>
<Label className="text-base font-semibold mb-2 block">Cost Center</Label>
<Input
value={newHubDepartment.cost_center}
onChange={(e) => setNewHubDepartment({ ...newHubDepartment, cost_center: e.target.value })}
placeholder="CC-12345"
className="h-11 font-mono"
/>
</div>
<div className="bg-amber-50 border-l-4 border-amber-400 p-4 rounded-r-lg">
<p className="text-sm text-amber-900">
<strong>Note:</strong> Team members can be assigned to this department when invited or during profile setup.
</p>
</div>
</div>
<DialogFooter className="border-t pt-4">
<Button variant="outline" size="lg" onClick={() => {
setShowAddHubDepartmentDialog(false);
setNewHubDepartment({ department_name: "", cost_center: "" });
}}>Cancel</Button>
<Button
size="lg"
onClick={async () => {
const updatedDepartments = [...(selectedHubForDept.departments || []), newHubDepartment];
await base44.entities.TeamHub.update(selectedHubForDept.id, {
departments: updatedDepartments
});
// Also add department to team's global department list
const teamDepartments = userTeam?.departments || [];
if (!teamDepartments.includes(newHubDepartment.department_name)) {
await base44.entities.Team.update(userTeam.id, {
departments: [...teamDepartments, newHubDepartment.department_name]
});
queryClient.invalidateQueries({ queryKey: ['user-team', user?.id, userRole] });
}
queryClient.invalidateQueries({ queryKey: ['team-hubs-main', userTeam?.id] });
setShowAddHubDepartmentDialog(false);
setNewHubDepartment({ department_name: "", cost_center: "" });
toast({ title: "✅ Department Added", description: `Department added to ${selectedHubForDept.hub_name}` });
}}
className="bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white"
disabled={!newHubDepartment.department_name}
>
<Plus className="w-5 h-5 mr-2" />
Add Department
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Add Favorite Staff Dialog */}
<Dialog open={showAddFavoriteDialog} onOpenChange={setShowAddFavoriteDialog}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Add Favorite Staff</DialogTitle>
</DialogHeader>
<div className="max-h-96 overflow-y-auto space-y-2">
{allStaff.filter(s => !(userTeam?.favorite_staff || []).some(f => f.staff_id === s.id)).map((staff) => (
<Card key={staff.id} className="cursor-pointer hover:bg-blue-50 transition-colors" onClick={() => addToFavorites(staff)}>
<CardContent className="p-4">
<div className="flex items-center gap-3">
<Avatar className="w-10 h-10">
<AvatarFallback className="bg-[#0A39DF] text-white">
{staff.employee_name?.charAt(0)}
</AvatarFallback>
</Avatar>
<div>
<p className="font-semibold">{staff.employee_name}</p>
<p className="text-xs text-slate-500">{staff.position}</p>
</div>
</div>
</CardContent>
</Card>
))}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowAddFavoriteDialog(false)}>Cancel</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Add Blocked Staff Dialog */}
<Dialog open={showAddBlockedDialog} onOpenChange={setShowAddBlockedDialog}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Block Staff Member</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Reason for blocking *</Label>
<Input
value={blockReason}
onChange={(e) => setBlockReason(e.target.value)}
placeholder="Performance issues, policy violation, etc."
/>
</div>
<div className="max-h-64 overflow-y-auto space-y-2">
{allStaff.filter(s => !(userTeam?.blocked_staff || []).some(b => b.staff_id === s.id)).map((staff) => (
<Card key={staff.id} className="cursor-pointer hover:bg-red-50 transition-colors" onClick={() => addToBlocked(staff)}>
<CardContent className="p-4">
<div className="flex items-center gap-3">
<Avatar className="w-10 h-10">
<AvatarFallback className="bg-slate-200 text-slate-700">
{staff.employee_name?.charAt(0)}
</AvatarFallback>
</Avatar>
<div>
<p className="font-semibold">{staff.employee_name}</p>
<p className="text-xs text-slate-500">{staff.position}</p>
</div>
</div>
</CardContent>
</Card>
))}
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => {
setShowAddBlockedDialog(false);
setBlockReason("");
}}>Cancel</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</div>
</div>
);
}