import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import {
Users,
Plus,
UserPlus,
Trash2,
AlertTriangle,
CheckCircle2,
Clock,
Mail,
Loader2,
} from 'lucide-react';
import { teamApi, type TeamMember, type Invite } from '../../api/settingsApi';
/* βββ Styling constants (match SettingsPage) βββββββββββββββββββββββ */
const inputClass =
'w-full rounded-lg border border-surface-300 py-2 px-3 text-sm outline-none focus:border-brand-500 focus:ring-2 focus:ring-brand-100';
const selectClass =
'w-full rounded-lg border border-surface-300 py-2 px-3 text-sm outline-none focus:border-brand-500 focus:ring-2 focus:ring-brand-100 bg-white';
const btnClass =
'rounded-lg bg-brand-600 py-2 px-4 text-sm font-semibold text-white hover:bg-brand-700 disabled:opacity-50';
const btnDangerClass =
'rounded-lg bg-red-600 py-2 px-4 text-sm font-semibold text-white hover:bg-red-700 disabled:opacity-50';
const btnSmClass =
'rounded-md border border-surface-300 px-2.5 py-1 text-xs font-medium text-surface-700 hover:bg-surface-50';
const btnDangerSmClass =
'rounded-md px-2.5 py-1 text-xs font-medium text-red-600 hover:bg-red-50 border border-transparent hover:border-red-200';
const ROLES: { value: string; label: string }[] = [
{ value: 'owner', label: 'Owner' },
{ value: 'admin', label: 'Admin' },
{ value: 'manager', label: 'Manager' },
{ value: 'member', label: 'Member' },
{ value: 'viewer', label: 'Viewer' },
];
/* βββ Role badge ββββββββββββββββββββββββββββββββββββββββββββββββββββ */
function RoleBadge({ role }: { role: string }) {
const colorMap: Record<string, string> = {
owner: 'bg-purple-100 text-purple-700',
admin: 'bg-red-100 text-red-700',
manager: 'bg-blue-100 text-blue-700',
member: 'bg-green-100 text-green-700',
viewer: 'bg-surface-100 text-surface-600',
};
return (
<span
className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${
colorMap[role] || colorMap.viewer
}`}
>
{role}
</span>
);
}
/* βββ Status badge ββββββββββββββββββββββββββββββββββββββββββββββββββ */
function StatusBadge({ status }: { status: string }) {
const isActive = status.toLowerCase() === 'active';
return (
<span
className={`inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-xs font-medium ${
isActive ? 'bg-green-100 text-green-700' : 'bg-surface-100 text-surface-500'
}`}
>
<span className={`h-1.5 w-1.5 rounded-full ${isActive ? 'bg-green-500' : 'bg-surface-400'}`} />
{status}
</span>
);
}
/* βββ Confirmation modal ββββββββββββββββββββββββββββββββββββββββββββ */
interface ConfirmModalProps {
open: boolean;
title: string;
message: string;
confirmLabel: string;
danger?: boolean;
onClose: () => void;
onConfirm: () => void;
isPending: boolean;
}
function ConfirmModal({ open, title, message, confirmLabel, danger, onClose, onConfirm, isPending }: ConfirmModalProps) {
if (!open) return null;
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/40"
onClick={onClose}
>
<div
className="w-full max-w-md rounded-xl border border-surface-200 bg-white p-6 shadow-xl"
onClick={(e) => e.stopPropagation()}
>
<div className="mb-4 flex items-start gap-3">
<div className={`rounded-lg p-2 ${danger ? 'bg-red-100' : 'bg-brand-100'}`}>
<AlertTriangle className={`h-5 w-5 ${danger ? 'text-red-600' : 'text-brand-600'}`} />
</div>
<div>
<h3 className="font-semibold text-surface-900">{title}</h3>
<p className="mt-1 text-sm text-surface-500">{message}</p>
</div>
</div>
<div className="flex items-center justify-end gap-3">
<button type="button" onClick={onClose} className={btnSmClass} disabled={isPending}>
Cancel
</button>
<button
type="button"
onClick={onConfirm}
className={danger ? btnDangerClass : btnClass}
disabled={isPending}
>
{isPending ? (
<span className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
Processingβ¦
</span>
) : (
confirmLabel
)}
</button>
</div>
</div>
</div>
);
}
/* βββ TeamTab βββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
export default function TeamTab() {
const queryClient = useQueryClient();
// βββ Invite form state βββββββββββββββββββββββββββββββββββββββββ
const [inviteEmail, setInviteEmail] = useState('');
const [inviteRole, setInviteRole] = useState('member');
// βββ Confirm modal state βββββββββββββββββββββββββββββββββββββββ
type ConfirmAction =
| { type: 'update-role'; member: TeamMember; newRole: string }
| { type: 'remove'; member: TeamMember }
| { type: 'cancel-invite'; invite: Invite }
| null;
const [confirmAction, setConfirmAction] = useState<ConfirmAction>(null);
// βββ Queries βββββββββββββββββββββββββββββββββββββββββββββββββββ
const { data: teamData, isLoading: teamLoading } = useQuery({
queryKey: ['team'],
queryFn: teamApi.getTeam,
staleTime: 30_000,
});
const { data: invitesData, isLoading: invitesLoading } = useQuery({
queryKey: ['invites'],
queryFn: teamApi.getInvites,
staleTime: 30_000,
});
// βββ Mutations βββββββββββββββββββββββββββββββββββββββββββββββββ
const updateRoleMutation = useMutation({
mutationFn: ({ userId, role }: { userId: number; role: string }) =>
teamApi.updateTeamMember(userId, { role }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['team'] });
},
});
const removeMemberMutation = useMutation({
mutationFn: (userId: number) => teamApi.removeTeamMember(userId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['team'] });
},
});
const sendInviteMutation = useMutation({
mutationFn: (data: { email: string; role: string }) => teamApi.sendInvite(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['invites'] });
setInviteEmail('');
setInviteRole('member');
},
});
const cancelInviteMutation = useMutation({
mutationFn: (inviteId: number) => teamApi.cancelInvite(inviteId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['invites'] });
},
});
// βββ Handlers ββββββββββββββββββββββββββββββββββββββββββββββββββ
const handleSendInvite = () => {
if (!inviteEmail.trim()) return;
sendInviteMutation.mutate({ email: inviteEmail.trim(), role: inviteRole });
};
const handleConfirm = () => {
if (!confirmAction) return;
switch (confirmAction.type) {
case 'update-role':
updateRoleMutation.mutate({
userId: confirmAction.member.id,
role: confirmAction.newRole,
});
break;
case 'remove':
removeMemberMutation.mutate(confirmAction.member.id);
break;
case 'cancel-invite':
cancelInviteMutation.mutate(confirmAction.invite.id);
break;
}
setConfirmAction(null);
};
const formatDate = (iso: string | null) => {
if (!iso) return 'β';
const d = new Date(iso);
return d.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
});
};
const isPendingAction = (action: ConfirmAction) => {
if (!action) return false;
switch (action.type) {
case 'update-role':
return updateRoleMutation.isPending;
case 'remove':
return removeMemberMutation.isPending;
case 'cancel-invite':
return cancelInviteMutation.isPending;
}
};
// βββ Loading state βββββββββββββββββββββββββββββββββββββββββββββ
if (teamLoading || invitesLoading) {
return (
<div className="rounded-xl border border-surface-200 bg-white p-6">
<div className="flex items-center gap-3 text-surface-500">
<Loader2 className="h-5 w-5 animate-spin" />
<span className="text-sm">Loading team informationβ¦</span>
</div>
</div>
);
}
const members = teamData?.items ?? [];
const invites = invitesData?.invites ?? [];
return (
<div className="space-y-6">
{/* βββ Header ββββββββββββββββββββββββββββββββββββββββββββ */}
<div>
<h3 className="text-lg font-semibold text-surface-900">Team Management</h3>
<p className="text-sm text-surface-500 mt-1">
Invite team members, manage roles, and control access.
</p>
</div>
{/* βββ Invite form βββββββββββββββββββββββββββββββββββββββ */}
<div className="rounded-xl border border-surface-200 bg-white p-6">
<div className="flex items-start gap-4">
<div className="rounded-lg bg-brand-100 p-3 text-brand-700">
<UserPlus className="h-5 w-5" />
</div>
<div className="flex-1">
<h4 className="font-medium text-surface-900">Invite a Team Member</h4>
<p className="text-sm text-surface-500 mt-1">
Send an invitation email to add someone to your team.
</p>
<div className="mt-4 flex flex-col gap-3 sm:flex-row sm:items-end">
<div className="flex-1">
<label className="mb-1 block text-xs font-medium text-surface-600">
Email address
</label>
<div className="relative">
<Mail className="absolute left-3 top-2.5 h-4 w-4 text-surface-400" />
<input
type="email"
value={inviteEmail}
onChange={(e) => setInviteEmail(e.target.value)}
placeholder="colleague@example.com"
className={inputClass + ' pl-9'}
disabled={sendInviteMutation.isPending}
onKeyDown={(e) => {
if (e.key === 'Enter') handleSendInvite();
}}
/>
</div>
</div>
<div className="sm:w-44">
<label className="mb-1 block text-xs font-medium text-surface-600">
Role
</label>
<select
value={inviteRole}
onChange={(e) => setInviteRole(e.target.value)}
className={selectClass}
disabled={sendInviteMutation.isPending}
>
{ROLES.map((r) => (
<option key={r.value} value={r.value}>
{r.label}
</option>
))}
</select>
</div>
<button
type="button"
onClick={handleSendInvite}
className={btnClass}
disabled={sendInviteMutation.isPending || !inviteEmail.trim()}
>
{sendInviteMutation.isPending ? (
<span className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin" />
Sendingβ¦
</span>
) : (
<span className="flex items-center gap-2">
<Plus className="h-4 w-4" />
Send Invite
</span>
)}
</button>
</div>
{sendInviteMutation.isSuccess && (
<div className="mt-3 flex items-center gap-2 rounded-lg bg-green-50 p-3 text-sm text-green-700">
<CheckCircle2 className="h-4 w-4 shrink-0" />
Invitation sent successfully.
</div>
)}
{sendInviteMutation.error && (
<div className="mt-3 flex items-center gap-2 rounded-lg bg-red-50 p-3 text-sm text-red-700">
<AlertTriangle className="h-4 w-4 shrink-0" />
{(sendInviteMutation.error as { response?: { data?: { error?: string } } })?.response
?.data?.error || 'Failed to send invitation.'}
</div>
)}
</div>
</div>
</div>
{/* βββ Team members table ββββββββββββββββββββββββββββββββ */}
<div className="rounded-xl border border-surface-200 bg-white p-6">
<div className="mb-4 flex items-center justify-between">
<h4 className="font-medium text-surface-900">
Team Members{' '}
<span className="ml-1 text-sm font-normal text-surface-500">
({members.length} of {teamData?.total ?? 0})
</span>
</h4>
</div>
{members.length === 0 ? (
<div className="flex flex-col items-center justify-center py-10 text-surface-400">
<Users className="mb-3 h-10 w-10" />
<p className="text-sm">No team members yet.</p>
<p className="text-xs">Use the form above to invite your first member.</p>
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-surface-200">
<th className="pb-3 text-left text-xs font-medium uppercase tracking-wider text-surface-500">
Name
</th>
<th className="pb-3 text-left text-xs font-medium uppercase tracking-wider text-surface-500">
Email
</th>
<th className="pb-3 text-left text-xs font-medium uppercase tracking-wider text-surface-500">
Role
</th>
<th className="pb-3 text-left text-xs font-medium uppercase tracking-wider text-surface-500">
Status
</th>
<th className="pb-3 text-left text-xs font-medium uppercase tracking-wider text-surface-500">
Last Login
</th>
<th className="pb-3 text-right text-xs font-medium uppercase tracking-wider text-surface-500">
Actions
</th>
</tr>
</thead>
<tbody className="divide-y divide-surface-100">
{members.map((member) => (
<tr key={member.id} className="group">
<td className="py-3 pr-4 font-medium text-surface-900">
{member.name}
</td>
<td className="py-3 pr-4 text-surface-600">{member.email}</td>
<td className="py-3 pr-4">
<RoleBadge role={member.role} />
</td>
<td className="py-3 pr-4">
<StatusBadge status={member.status} />
</td>
<td className="py-3 pr-4 text-surface-500">
<span className="flex items-center gap-1">
<Clock className="h-3.5 w-3.5" />
{formatDate(member.lastLogin)}
</span>
</td>
<td className="py-3 text-right">
<div className="flex items-center justify-end gap-2 opacity-0 transition-opacity group-hover:opacity-100">
{/* Role change dropdown */}
<select
value={member.role}
onChange={(e) => {
if (e.target.value !== member.role) {
setConfirmAction({
type: 'update-role',
member,
newRole: e.target.value,
});
}
}}
className={btnSmClass + ' h-7 w-auto text-xs'}
>
<option value="">Change roleβ¦</option>
{ROLES.map((r) => (
<option key={r.value} value={r.value}>
{r.label}
</option>
))}
</select>
{/* Remove button */}
<button
type="button"
onClick={() =>
setConfirmAction({ type: 'remove', member })
}
className={btnDangerSmClass}
title="Remove member"
>
<Trash2 className="h-3.5 w-3.5" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
{/* βββ Pending invites βββββββββββββββββββββββββββββββββββ */}
<div className="rounded-xl border border-surface-200 bg-white p-6">
<h4 className="mb-4 font-medium text-surface-900">
Pending Invitations{' '}
<span className="text-sm font-normal text-surface-500">({invites.length})</span>
</h4>
{invites.length === 0 ? (
<div className="flex flex-col items-center justify-center py-10 text-surface-400">
<Mail className="mb-3 h-10 w-10" />
<p className="text-sm">No pending invitations.</p>
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-surface-200">
<th className="pb-3 text-left text-xs font-medium uppercase tracking-wider text-surface-500">
Email
</th>
<th className="pb-3 text-left text-xs font-medium uppercase tracking-wider text-surface-500">
Role
</th>
<th className="pb-3 text-left text-xs font-medium uppercase tracking-wider text-surface-500">
Sent
</th>
<th className="pb-3 text-left text-xs font-medium uppercase tracking-wider text-surface-500">
Expires
</th>
<th className="pb-3 text-right text-xs font-medium uppercase tracking-wider text-surface-500">
Actions
</th>
</tr>
</thead>
<tbody className="divide-y divide-surface-100">
{invites.map((invite) => (
<tr key={invite.id} className="group">
<td className="py-3 pr-4 text-surface-900">{invite.email}</td>
<td className="py-3 pr-4">
<RoleBadge role={invite.role} />
</td>
<td className="py-3 pr-4 text-surface-500">
{formatDate(invite.created_at)}
</td>
<td className="py-3 pr-4 text-surface-500">
{formatDate(invite.expires_at)}
</td>
<td className="py-3 text-right">
<button
type="button"
onClick={() =>
setConfirmAction({ type: 'cancel-invite', invite })
}
className={btnDangerSmClass}
title="Cancel invitation"
>
Cancel
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
{/* βββ Confirmation modal ββββββββββββββββββββββββββββββββ */}
{confirmAction && (
<ConfirmModal
open={!!confirmAction}
isPending={isPendingAction(confirmAction)}
onClose={() => setConfirmAction(null)}
onConfirm={handleConfirm}
{...(() => {
switch (confirmAction.type) {
case 'update-role':
return {
title: 'Change Role',
message: `Change ${confirmAction.member.name}'s role to ${confirmAction.newRole}?`,
confirmLabel: 'Change Role',
};
case 'remove':
return {
title: 'Remove Team Member',
message: `Remove ${confirmAction.member.name} from the team? This cannot be undone.`,
confirmLabel: 'Remove',
danger: true,
};
case 'cancel-invite':
return {
title: 'Cancel Invitation',
message: `Cancel the invitation for ${confirmAction.invite.email}?`,
confirmLabel: 'Cancel Invite',
};
}
})()}
/>
)}
</div>
);
}