'use client' import { useState, useEffect, useCallback } from 'react' import { useTranslations } from 'next-intl' import { Button } from '@/components/ui/button' import { useToast } from '@/components/ui/use-toast' import { useCompany } from '@/contexts/CompanyContext' import { SettingsGroup, SettingsInput, SettingsRowNote, SettingsSelect, } from '@/components/settings/SettingsRows' import { formatDateLong } from '@/lib/utils' import { Loader2, Plus, Trash2, Mail } from 'lucide-react' interface CompanyMemberItem { id: string user_id: string email: string role: string source: 'direct' | 'team' joined_at: string is_current_user: boolean } interface CompanyInvitation { id: string email: string role: string status: string expires_at: string created_at: string } export function CompanyMembersSection() { const t = useTranslations('settings_company') const { toast } = useToast() const { company } = useCompany() const roleLabels: Record = { owner: t('members_role_owner'), admin: t('members_role_admin'), member: t('members_role_member'), viewer: t('members_role_viewer'), } const [isLoading, setIsLoading] = useState(true) const [members, setMembers] = useState([]) const [invitations, setInvitations] = useState([]) const [inviteEmail, setInviteEmail] = useState('') const [inviteRole, setInviteRole] = useState('viewer') const [isSending, setIsSending] = useState(false) const [removingId, setRemovingId] = useState(null) const [revokingId, setRevokingId] = useState(null) const [canInvite, setCanInvite] = useState(false) const fetchMembers = useCallback(async () => { try { const res = await fetch('/api/company/members') const data = await res.json() if (res.ok) { setMembers(data.data.members) setInvitations(data.data.invitations) setCanInvite(data.data.canInvite) } } catch { // Silently fail } finally { setIsLoading(false) } }, []) useEffect(() => { fetchMembers() }, [fetchMembers]) const handleInvite = async (e: React.FormEvent) => { e.preventDefault() const email = inviteEmail.trim().toLowerCase() if (!email) return setIsSending(true) try { const res = await fetch('/api/company/members/invite', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, role: inviteRole }), }) const data = await res.json() if (!res.ok) { toast({ title: data.error, variant: 'destructive' }) return } if (data.data.inviteUrl) { console.log('[DEV] Company invite URL:', data.data.inviteUrl) } toast({ title: t('members_invite_sent_title'), description: data.data.inviteUrl ? t('members_invite_sent_dev_url') : t('members_invite_sent_description', { email }), }) setInviteEmail('') setInviteRole('viewer') fetchMembers() } catch { toast({ title: t('members_invite_failed'), variant: 'destructive' }) } finally { setIsSending(false) } } const handleRemoveMember = async (memberId: string) => { setRemovingId(memberId) try { const res = await fetch(`/api/company/members/${memberId}`, { method: 'DELETE' }) const data = await res.json() if (!res.ok) { toast({ title: data.error, variant: 'destructive' }) return } toast({ title: t('members_removed') }) fetchMembers() } catch { toast({ title: t('members_remove_failed'), variant: 'destructive' }) } finally { setRemovingId(null) } } const handleRevokeInvite = async (inviteId: string) => { setRevokingId(inviteId) try { const res = await fetch(`/api/company/members/invite/${inviteId}`, { method: 'DELETE' }) const data = await res.json() if (!res.ok) { toast({ title: data.error, variant: 'destructive' }) return } toast({ title: t('members_invite_revoked') }) fetchMembers() } catch { toast({ title: t('members_invite_revoke_failed'), variant: 'destructive' }) } finally { setRevokingId(null) } } if (isLoading) { return (
) } return ( {/* Member rows: flat hairline list, no cards. */} {members.map((member) => (
{member.email.charAt(0).toUpperCase()}

{member.email} {member.is_current_user && ( {t('members_you')} )}

{roleLabels[member.role] || member.role} {member.source === 'team' && <> · {t('members_team_badge')}} {canInvite && !member.is_current_user && member.role !== 'owner' && member.source !== 'team' && ( )}
))} {/* Pending invitations continue the same list, visually quieter. */} {invitations.map((inv) => (

{inv.email} · {t('invitations_expires', { date: formatDateLong(inv.expires_at) })}

{roleLabels[inv.role] || inv.role} {canInvite && ( )}
))} {/* Inline invite: the list's own last row instead of a separate card. */} {canInvite && (
setInviteEmail(e.target.value)} disabled={isSending} required className="border-border sm:flex-1" /> setInviteRole(e.target.value)} aria-label={t('members_role_label')} > )}

{t('members_count', { count: members.length, companyName: company?.name ?? '' })}

) }