/**
* Team/Organization Management Component
*
* Allows users to:
* - View their organizations
* - Create new organizations
* - Manage members (invite, remove, change roles)
* - Leave organizations
*/
'use client';
import { useState, useEffect } from 'react';
import Image from 'next/image';
import {
Users,
Plus,
Crown,
Shield,
User,
Eye,
Mail,
Copy,
Check,
X,
Loader2,
ChevronDown,
ChevronRight,
Trash2,
LogOut,
Settings,
UserPlus,
Building2,
} from 'lucide-react';
import type {
OrganizationWithMembers,
OrganizationMember,
OrganizationRole,
OrganizationInvite,
} from '@/lib/types/organization';
import { ROLE_LABELS, ROLE_DESCRIPTIONS } from '@/lib/types/organization';
interface TeamManagementProps {
userId: string;
}
export function TeamManagement({ userId }: TeamManagementProps) {
// Organizations state
const [organizations, setOrganizations] = useState<OrganizationWithMembers[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
// Selected org state
const [selectedOrgId, setSelectedOrgId] = useState<string | null>(null);
const [selectedOrg, setSelectedOrg] = useState<OrganizationWithMembers | null>(null);
const [orgLoading, setOrgLoading] = useState(false);
// Create org state
const [showCreateForm, setShowCreateForm] = useState(false);
const [newOrgName, setNewOrgName] = useState('');
const [createLoading, setCreateLoading] = useState(false);
const [createError, setCreateError] = useState('');
// Invite state
const [showInviteForm, setShowInviteForm] = useState(false);
const [inviteEmail, setInviteEmail] = useState('');
const [inviteRole, setInviteRole] = useState<OrganizationRole>('member');
const [inviteLoading, setInviteLoading] = useState(false);
const [inviteError, setInviteError] = useState('');
const [inviteSuccess, setInviteSuccess] = useState('');
const [copiedInviteUrl, setCopiedInviteUrl] = useState(false);
const [pendingInvites, setPendingInvites] = useState<any[]>([]);
// Load organizations
useEffect(() => {
fetchOrganizations();
}, []);
// Load selected org details
useEffect(() => {
if (selectedOrgId) {
fetchOrganizationDetails(selectedOrgId);
} else {
setSelectedOrg(null);
}
}, [selectedOrgId]);
const fetchOrganizations = async () => {
setLoading(true);
setError('');
try {
const response = await fetch('/api/organizations');
if (!response.ok) {
throw new Error('Failed to fetch organizations');
}
const data = await response.json();
setOrganizations(data.organizations || []);
} catch (err: any) {
setError(err.message || 'Failed to load organizations');
} finally {
setLoading(false);
}
};
const fetchOrganizationDetails = async (orgId: string) => {
setOrgLoading(true);
try {
const [orgResponse, invitesResponse] = await Promise.all([
fetch(`/api/organizations/${orgId}`),
fetch(`/api/organizations/${orgId}/invites`),
]);
if (orgResponse.ok) {
const data = await orgResponse.json();
setSelectedOrg(data.organization);
}
if (invitesResponse.ok) {
const invData = await invitesResponse.json();
setPendingInvites(invData.invites || []);
}
} catch (err) {
console.error('Failed to fetch org details:', err);
} finally {
setOrgLoading(false);
}
};
const handleCreateOrg = async (e: React.FormEvent) => {
e.preventDefault();
if (!newOrgName.trim()) return;
setCreateLoading(true);
setCreateError('');
try {
const response = await fetch('/api/organizations', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: newOrgName.trim() }),
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || 'Failed to create organization');
}
const data = await response.json();
setOrganizations([...organizations, data.organization]);
setNewOrgName('');
setShowCreateForm(false);
setSelectedOrgId(data.organization.id);
} catch (err: any) {
setCreateError(err.message);
} finally {
setCreateLoading(false);
}
};
const handleInvite = async (e: React.FormEvent) => {
e.preventDefault();
if (!inviteEmail.trim() || !selectedOrgId) return;
setInviteLoading(true);
setInviteError('');
setInviteSuccess('');
try {
const response = await fetch(`/api/organizations/${selectedOrgId}/invites`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: inviteEmail.trim(), role: inviteRole }),
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || 'Failed to send invite');
}
const data = await response.json();
setInviteSuccess(`Invite sent! Share this link: ${data.invite_url}`);
setInviteEmail('');
setPendingInvites([...pendingInvites, data.invite]);
} catch (err: any) {
setInviteError(err.message);
} finally {
setInviteLoading(false);
}
};
const handleCopyInviteUrl = async (url: string) => {
await navigator.clipboard.writeText(url);
setCopiedInviteUrl(true);
setTimeout(() => setCopiedInviteUrl(false), 2000);
};
const handleRevokeInvite = async (inviteId: string) => {
if (!selectedOrgId) return;
try {
const response = await fetch(`/api/organizations/${selectedOrgId}/invites/${inviteId}`, {
method: 'DELETE',
});
if (response.ok) {
setPendingInvites(pendingInvites.filter((inv) => inv.id !== inviteId));
}
} catch (err) {
console.error('Failed to revoke invite:', err);
}
};
const handleRemoveMember = async (memberId: string, memberUserId: string) => {
if (!selectedOrgId) return;
if (!confirm('Are you sure you want to remove this member?')) return;
try {
const response = await fetch(`/api/organizations/${selectedOrgId}/members?userId=${memberUserId}`, {
method: 'DELETE',
});
if (response.ok) {
if (memberUserId === userId) {
// Left the org
setOrganizations(organizations.filter((org) => org.id !== selectedOrgId));
setSelectedOrgId(null);
} else {
// Removed someone else
fetchOrganizationDetails(selectedOrgId);
}
} else {
const data = await response.json();
alert(data.error || 'Failed to remove member');
}
} catch (err) {
console.error('Failed to remove member:', err);
}
};
const handleUpdateRole = async (memberUserId: string, newRole: OrganizationRole) => {
if (!selectedOrgId) return;
try {
const response = await fetch(`/api/organizations/${selectedOrgId}/members/${memberUserId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ role: newRole }),
});
if (response.ok) {
fetchOrganizationDetails(selectedOrgId);
} else {
const data = await response.json();
alert(data.error || 'Failed to update role');
}
} catch (err) {
console.error('Failed to update role:', err);
}
};
const handleDeleteOrg = async () => {
if (!selectedOrgId || !selectedOrg) return;
if (!confirm(`Are you sure you want to delete "${selectedOrg.name}"? This action cannot be undone.`)) return;
try {
const response = await fetch(`/api/organizations/${selectedOrgId}`, {
method: 'DELETE',
});
if (response.ok) {
setOrganizations(organizations.filter((org) => org.id !== selectedOrgId));
setSelectedOrgId(null);
} else {
const data = await response.json();
alert(data.error || 'Failed to delete organization');
}
} catch (err) {
console.error('Failed to delete org:', err);
}
};
const getRoleIcon = (role: OrganizationRole) => {
switch (role) {
case 'admin':
return <Crown className="w-4 h-4 text-yellow-500" />;
case 'manager':
return <Shield className="w-4 h-4 text-blue-500" />;
case 'member':
return <User className="w-4 h-4 text-gray-500" />;
case 'viewer':
return <Eye className="w-4 h-4 text-gray-400" />;
}
};
const canManageMembers = selectedOrg?.current_user_role === 'admin' || selectedOrg?.current_user_role === 'manager';
const isAdmin = selectedOrg?.current_user_role === 'admin';
if (loading) {
return (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-8 h-8 animate-spin text-gray-400" />
</div>
);
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-medium text-gray-900">Teams & Organizations</h3>
<p className="text-sm text-gray-500">
Manage your team memberships and collaborate with others
</p>
</div>
<button
onClick={() => setShowCreateForm(true)}
className="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
>
<Plus className="w-4 h-4" />
Create Team
</button>
</div>
{error && (
<div className="p-4 bg-red-50 border border-red-200 rounded-md">
<p className="text-red-800 text-sm">{error}</p>
</div>
)}
{/* Create Organization Form */}
{showCreateForm && (
<div className="p-4 bg-gray-50 border border-gray-200 rounded-lg">
<form onSubmit={handleCreateOrg} className="space-y-4">
<div>
<label htmlFor="orgName" className="block text-sm font-medium text-gray-700 mb-1">
Team Name
</label>
<input
type="text"
id="orgName"
value={newOrgName}
onChange={(e) => setNewOrgName(e.target.value)}
placeholder="My Team"
className="w-full px-3 py-2 border border-gray-300 rounded-md text-gray-900 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
autoFocus
/>
</div>
{createError && (
<p className="text-sm text-red-600">{createError}</p>
)}
<div className="flex gap-2">
<button
type="submit"
disabled={createLoading || !newOrgName.trim()}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed"
>
{createLoading ? 'Creating...' : 'Create Team'}
</button>
<button
type="button"
onClick={() => {
setShowCreateForm(false);
setNewOrgName('');
setCreateError('');
}}
className="px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300"
>
Cancel
</button>
</div>
</form>
</div>
)}
{/* Organizations List */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left: Org List */}
<div className="lg:col-span-1 space-y-2">
{organizations.length === 0 ? (
<div className="text-center py-8 text-gray-500">
<Building2 className="w-12 h-12 mx-auto mb-3 text-gray-300" />
<p>No teams yet</p>
<p className="text-sm">Create a team to get started</p>
</div>
) : (
organizations.map((org) => (
<button
key={org.id}
onClick={() => setSelectedOrgId(org.id)}
className={`w-full text-left p-4 rounded-lg border transition-colors ${
selectedOrgId === org.id
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 bg-white hover:bg-gray-50'
}`}
>
<div className="flex items-center justify-between">
<div>
<h4 className="font-medium text-gray-900">{org.name}</h4>
<p className="text-sm text-gray-500">
{org.member_count} {org.member_count === 1 ? 'member' : 'members'}
</p>
</div>
<div className="flex items-center gap-2">
{getRoleIcon(org.current_user_role!)}
<span className="text-xs text-gray-500">
{ROLE_LABELS[org.current_user_role!]}
</span>
<ChevronRight className="w-4 h-4 text-gray-400" />
</div>
</div>
</button>
))
)}
</div>
{/* Right: Org Details */}
<div className="lg:col-span-2">
{selectedOrg ? (
<div className="bg-white border border-gray-200 rounded-lg p-6 space-y-6">
{orgLoading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="w-6 h-6 animate-spin text-gray-400" />
</div>
) : (
<>
{/* Org Header */}
<div className="flex items-center justify-between">
<div>
<h3 className="text-xl font-semibold text-gray-900">{selectedOrg.name}</h3>
<p className="text-sm text-gray-500">
Your role: {ROLE_LABELS[selectedOrg.current_user_role!]}
</p>
</div>
<div className="flex gap-2">
{canManageMembers && (
<button
onClick={() => setShowInviteForm(true)}
className="inline-flex items-center gap-2 px-3 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm"
>
<UserPlus className="w-4 h-4" />
Invite
</button>
)}
{isAdmin && (
<button
onClick={handleDeleteOrg}
className="inline-flex items-center gap-2 px-3 py-2 bg-red-100 text-red-700 rounded-md hover:bg-red-200 text-sm"
>
<Trash2 className="w-4 h-4" />
Delete
</button>
)}
</div>
</div>
{/* Invite Form */}
{showInviteForm && canManageMembers && (
<div className="p-4 bg-gray-50 border border-gray-200 rounded-lg">
<form onSubmit={handleInvite} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label htmlFor="inviteEmail" className="block text-sm font-medium text-gray-700 mb-1">
Email Address
</label>
<input
type="email"
id="inviteEmail"
value={inviteEmail}
onChange={(e) => setInviteEmail(e.target.value)}
placeholder="colleague@example.com"
className="w-full px-3 py-2 border border-gray-300 rounded-md text-gray-900 focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label htmlFor="inviteRole" className="block text-sm font-medium text-gray-700 mb-1">
Role
</label>
<select
id="inviteRole"
value={inviteRole}
onChange={(e) => setInviteRole(e.target.value as OrganizationRole)}
className="w-full px-3 py-2 border border-gray-300 rounded-md text-gray-900 focus:ring-2 focus:ring-blue-500"
>
{isAdmin && <option value="admin">Admin</option>}
<option value="manager">Manager</option>
<option value="member">Member</option>
<option value="viewer">Viewer</option>
</select>
</div>
</div>
{inviteError && <p className="text-sm text-red-600">{inviteError}</p>}
{inviteSuccess && (
<div className="flex items-center gap-2 text-sm text-green-600">
<Check className="w-4 h-4" />
{inviteSuccess}
</div>
)}
<div className="flex gap-2">
<button
type="submit"
disabled={inviteLoading || !inviteEmail.trim()}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed text-sm"
>
{inviteLoading ? 'Sending...' : 'Send Invite'}
</button>
<button
type="button"
onClick={() => {
setShowInviteForm(false);
setInviteEmail('');
setInviteError('');
setInviteSuccess('');
}}
className="px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 text-sm"
>
Cancel
</button>
</div>
</form>
</div>
)}
{/* Pending Invites */}
{canManageMembers && pendingInvites.length > 0 && (
<div>
<h4 className="text-sm font-medium text-gray-700 mb-3">Pending Invites</h4>
<div className="space-y-2">
{pendingInvites.map((invite) => (
<div
key={invite.id}
className="flex items-center justify-between p-3 bg-yellow-50 border border-yellow-200 rounded-md"
>
<div className="flex items-center gap-3">
<Mail className="w-4 h-4 text-yellow-600" />
<div>
<p className="text-sm font-medium text-gray-900">{invite.email}</p>
<p className="text-xs text-gray-500">
{ROLE_LABELS[invite.role as OrganizationRole]} • Expires{' '}
{new Date(invite.expires_at).toLocaleDateString()}
</p>
</div>
</div>
<button
onClick={() => handleRevokeInvite(invite.id)}
className="p-1 text-gray-400 hover:text-red-600"
title="Revoke invite"
>
<X className="w-4 h-4" />
</button>
</div>
))}
</div>
</div>
)}
{/* Members List */}
<div>
<h4 className="text-sm font-medium text-gray-700 mb-3">
Members ({selectedOrg.members.length})
</h4>
<div className="space-y-2">
{selectedOrg.members.map((member) => (
<div
key={member.id}
className="flex items-center justify-between p-3 bg-gray-50 rounded-md"
>
<div className="flex items-center gap-3">
{member.user?.avatar_url ? (
<Image
src={member.user.avatar_url}
alt=""
width={36}
height={36}
className="rounded-full"
/>
) : (
<div className="w-9 h-9 rounded-full bg-gray-300 flex items-center justify-center text-gray-600 font-medium">
{(member.user?.display_name || member.user?.full_name || member.user?.email)?.[0]?.toUpperCase() || '?'}
</div>
)}
<div>
<p className="text-sm font-medium text-gray-900">
{member.user?.display_name || member.user?.full_name || member.user?.email}
{member.user_id === userId && (
<span className="ml-2 text-xs text-gray-500">(you)</span>
)}
</p>
<p className="text-xs text-gray-500">{member.user?.email}</p>
</div>
</div>
<div className="flex items-center gap-3">
{canManageMembers && member.user_id !== userId ? (
<select
value={member.role}
onChange={(e) => handleUpdateRole(member.user_id, e.target.value as OrganizationRole)}
className="text-sm border border-gray-300 rounded px-2 py-1 text-gray-700"
disabled={!isAdmin && member.role === 'admin'}
>
{isAdmin && <option value="admin">Admin</option>}
<option value="manager">Manager</option>
<option value="member">Member</option>
<option value="viewer">Viewer</option>
</select>
) : (
<div className="flex items-center gap-1">
{getRoleIcon(member.role)}
<span className="text-sm text-gray-600">{ROLE_LABELS[member.role]}</span>
</div>
)}
{(canManageMembers && member.user_id !== userId) || member.user_id === userId ? (
<button
onClick={() => handleRemoveMember(member.id, member.user_id)}
className="p-1 text-gray-400 hover:text-red-600"
title={member.user_id === userId ? 'Leave team' : 'Remove member'}
>
{member.user_id === userId ? (
<LogOut className="w-4 h-4" />
) : (
<Trash2 className="w-4 h-4" />
)}
</button>
) : null}
</div>
</div>
))}
</div>
</div>
</>
)}
</div>
) : (
<div className="bg-gray-50 border border-gray-200 rounded-lg p-8 text-center">
<Users className="w-12 h-12 mx-auto mb-3 text-gray-300" />
<p className="text-gray-500">Select a team to view details</p>
</div>
)}
</div>
</div>
</div>
);
}