'use client'; import { useEffect, useState } from 'react'; import { Check, Trash2, UserPlus, X } from 'lucide-react'; import { useAuth } from '@/lib/hooks/useAuth'; import { useOrganizationInviteLinkCopy } from '@/lib/hooks/useOrganizationInviteLinkCopy'; import { organizationApi, type CounterpartItemDto, type CounterpartSearchResultDto, type OrganizationInvitationHistoryItemDto, } from '@/lib/api/organization'; import { invitationTargetFromConnectionRow } from '@/components/invitations/organizationInviteLinks'; import { CopyInvitationLinkButton } from '@/components/ui/organizations/CopyInvitationLinkButton'; import { InvitationHistoryDialog } from '@/components/ui/organizations/InvitationHistoryDialog'; import { Button } from '@/components/ui/common/Button'; import { Badge, organizationConnectionStatusVariant } from '@/components/ui/common/Badge'; import { Input } from '@/components/ui/common/Input'; import { SearchBar } from '@/components/ui/common/SearchBar'; import { Table } from '@/components/ui/common/Table'; import type { ApiError } from '@/types/api'; function formatOrganizationStatusLabel(status: string): string { if (!status) return status; const lower = status.toLowerCase(); return lower.charAt(0).toUpperCase() + lower.slice(1); } function formatConnectionStatusLabel( row: CounterpartItemDto, currentOrganizationId: string, ): string { if (row.status === 'PENDING') { if ( row.pendingInvitationId && row.requestedByOrganizationId === currentOrganizationId ) { return 'Invitation pending'; } return 'Connection request pending'; } if (row.status === 'ACTIVE') return 'Connected'; if (row.status === 'REJECTED') return 'Connection request declined'; return formatOrganizationStatusLabel(row.status); } function formatApiMessage(err: unknown): string { if (!err || typeof err !== 'object') return 'Something went wrong'; const m = (err as ApiError).message; if (Array.isArray(m)) return m.join(', '); if (typeof m === 'string') return m; return 'Something went wrong'; } function formatTableDate(value: string): string { const d = new Date(value); if (Number.isNaN(d.getTime())) return '\u2014'; return d.toLocaleDateString(); } type TableMode = 'existing' | 'search'; export default function OrganizationsPage() { const { currentOrganization } = useAuth(); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [success, setSuccess] = useState(''); const [query, setQuery] = useState(''); const [mode, setMode] = useState('existing'); const [searching, setSearching] = useState(false); const [searchResults, setSearchResults] = useState([]); const [pendingConnectionRowId, setPendingConnectionRowId] = useState(null); const [deleteConnectionRowId, setDeleteConnectionRowId] = useState(null); const [items, setItems] = useState([]); const [manualOrganizationName, setManualOrganizationName] = useState(''); const [manualOwnerEmail, setManualOwnerEmail] = useState(''); const [inviteLoading, setInviteLoading] = useState(false); const [showInviteForm, setShowInviteForm] = useState(false); const [historyOpen, setHistoryOpen] = useState(false); const [historyLoading, setHistoryLoading] = useState(false); const [historyItems, setHistoryItems] = useState([]); const [historyCopyError, setHistoryCopyError] = useState(''); const [historyCopySuccess, setHistoryCopySuccess] = useState(''); const { copiedId, copyingInvitationId, storeInviteLink, copyInvitationLink, pruneAcceptedLinks, } = useOrganizationInviteLinkCopy(currentOrganization?.id); const counterpartLabel = currentOrganization?.type === 'LAB' ? 'Clinic' : 'Lab'; const tabLabel = currentOrganization?.type === 'LAB' ? 'Clinics' : 'Labs'; const existingRows = items; async function loadList() { setLoading(true); setError(''); try { const res = await organizationApi.list(); setItems(res.data.items); } catch (e) { setError(formatApiMessage(e)); } finally { setLoading(false); } } useEffect(() => { void loadList(); }, []); useEffect(() => { if (!success) return; const t = setTimeout(() => setSuccess(''), 4000); return () => clearTimeout(t); }, [success]); async function runSearch() { const q = query.trim(); if (!q) { setMode('existing'); setSearchResults([]); setShowInviteForm(false); return; } setSearching(true); setError(''); setMode('search'); setShowInviteForm(false); try { const res = await organizationApi.search(q); setSearchResults(res.data); } catch (e) { setError(formatApiMessage(e)); setSearchResults([]); } finally { setSearching(false); } } async function submitConnectionRequest(targetOrganizationId: string) { setPendingConnectionRowId(targetOrganizationId); setError(''); try { await organizationApi.createConnectionRequest(targetOrganizationId); setSuccess(`${counterpartLabel} connection request sent.`); setSearchResults([]); setQuery(''); setMode('existing'); await loadList(); } catch (e) { setError(formatApiMessage(e)); } finally { setPendingConnectionRowId(null); } } async function sendInvite() { setInviteLoading(true); setError(''); try { const res = await organizationApi.invite({ organizationName: manualOrganizationName.trim(), ownerEmail: manualOwnerEmail.trim(), }); storeInviteLink(res.data.invitationId, manualOwnerEmail, res.data.invitationUrl); setSuccess(`Invitation link created for ${manualOwnerEmail.trim()}`); setManualOrganizationName(''); setManualOwnerEmail(''); setShowInviteForm(false); setMode('existing'); setQuery(''); setSearchResults([]); await loadList(); } catch (e) { setError(formatApiMessage(e)); } finally { setInviteLoading(false); } } async function loadInvitationHistory() { const res = await organizationApi.listInvitations(); setHistoryItems(res.data.items); pruneAcceptedLinks(res.data.items); return res.data.items; } async function openInvitationHistory() { setHistoryOpen(true); setHistoryLoading(true); setHistoryCopyError(''); setHistoryCopySuccess(''); setError(''); try { await loadInvitationHistory(); } catch (e) { setError(formatApiMessage(e)); } finally { setHistoryLoading(false); } } async function handleHistoryCopy(invitation: OrganizationInvitationHistoryItemDto) { setHistoryCopyError(''); setHistoryCopySuccess(''); try { await copyInvitationLink(invitation, { onRegenerated: async () => { await loadInvitationHistory(); }, }); setHistoryCopySuccess('Invitation link copied to clipboard.'); setTimeout(() => setHistoryCopySuccess(''), 3000); } catch (e) { setHistoryCopyError(formatApiMessage(e)); } } async function handleCopyInvitationFromRow(row: CounterpartItemDto) { const target = invitationTargetFromConnectionRow(row, currentOrganization!.id); if (!target) return; setError(''); try { await copyInvitationLink( { id: target.id, organizationName: row.organizationName, ownerEmail: target.ownerEmail, status: target.status, createdAt: row.createdAt, acceptedAt: target.acceptedAt, }, { onRegenerated: async () => { await loadList(); }, }, ); setSuccess('Invitation link copied to clipboard.'); setTimeout(() => setSuccess(''), 3000); } catch (e) { setError(formatApiMessage(e)); } } async function respondToPendingConnection(connectionId: string, action: 'ACCEPT' | 'REJECT') { setPendingConnectionRowId(connectionId); setError(''); try { await organizationApi.respondToConnectionRequest(connectionId, action); setSuccess( action === 'ACCEPT' ? 'Connection request accepted.' : 'Connection request declined.', ); await loadList(); } catch (e) { setError(formatApiMessage(e)); } finally { setPendingConnectionRowId(null); } } async function deleteConnection(connectionId: string) { setDeleteConnectionRowId(connectionId); setError(''); try { await organizationApi.deleteConnection(connectionId); setSuccess('Connection removed.'); await loadList(); } catch (e) { setError(formatApiMessage(e)); } finally { setDeleteConnectionRowId(null); } } function clearSearchView() { setMode('existing'); setQuery(''); setSearchResults([]); setShowInviteForm(false); } if (!currentOrganization) { return

Loading organization...

; } return (

{tabLabel}

Search organizations, send connection requests to existing accounts, or invitation links when they are not on DyoLink yet.

{error && (
{error}
)} {success && (
{success}
)} void runSearch()} placeholder={`Search ${counterpartLabel.toLowerCase()} by name, email, or phone...`} actions={ <> {mode === 'search' && ( )} } /> } body={ <> {loading ? ( ) : mode === 'existing' ? ( existingRows.length === 0 ? ( ) : ( existingRows.map((row) => { const canRespond = row.status === 'PENDING' && row.requestedByOrganizationId !== null && row.requestedByOrganizationId !== currentOrganization.id; const invitationTarget = invitationTargetFromConnectionRow( row, currentOrganization.id, ); return ( ); }) ) ) : searchResults.length > 0 ? ( searchResults.map((r) => ( )) ) : ( )} } /> { setHistoryOpen(false); setHistoryCopyError(''); setHistoryCopySuccess(''); }} loading={historyLoading} items={historyItems} copiedId={copiedId} copyingInvitationId={copyingInvitationId} onCopy={(invitation) => void handleHistoryCopy(invitation)} copyError={historyCopyError} copySuccess={historyCopySuccess} /> ); }
Organization Owner email Date Status Action
Loading...
No connections yet. Search to send a connection request or an invitation link.
{row.organizationName} {row.ownerEmail} {formatTableDate(row.createdAt)} {formatConnectionStatusLabel(row, currentOrganization.id)}
{invitationTarget && ( void handleCopyInvitationFromRow(row)} /> )} {canRespond && ( <> )} {row.status === 'ACTIVE' && ( )}
{r.name} {r.owner.email} Today Found

No organization found in directory search.

{showInviteForm && (
setManualOrganizationName(e.target.value)} /> setManualOwnerEmail(e.target.value)} />
)}