'use client'; import { useEffect, useState } from 'react'; import { Check, Copy, Link2, Trash2, X } from 'lucide-react'; import { useAuth } from '@/lib/hooks/useAuth'; import { organizationApi, type CounterpartItemDto, type CounterpartSearchResultDto, type OrganizationInvitationHistoryItemDto, } from '@/lib/api/organization'; import { Button } from '@/components/ui/common/Button'; import { Badge, organizationLinkStatusVariant } 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'; type StoredInviteLink = { invitationId: string; ownerEmail: string; invitationUrl: string; }; function inviteLinksStorageKey(orgId: string): string { return `counterpartInviteLinks:${orgId}`; } function readStoredInviteLinks(orgId: string): Record { if (typeof window === 'undefined') return {}; try { const raw = window.localStorage.getItem(inviteLinksStorageKey(orgId)); if (!raw) return {}; const parsed = JSON.parse(raw) as Record; return parsed && typeof parsed === 'object' ? parsed : {}; } catch { return {}; } } function writeStoredInviteLinks(orgId: string, links: Record) { if (typeof window === 'undefined') return; window.localStorage.setItem(inviteLinksStorageKey(orgId), JSON.stringify(links)); } function formatOrganizationStatusLabel(status: string): string { if (!status) return status; const lower = status.toLowerCase(); return lower.charAt(0).toUpperCase() + lower.slice(1); } function formatLinkStatusLabel(status: CounterpartItemDto['status']): string { if (status === 'PENDING') return 'Link request pending'; if (status === 'ACTIVE') return 'Linked'; if (status === 'REJECTED') return 'Link request rejected'; return formatOrganizationStatusLabel(status); } function formatInvitationStatusLabel(status: OrganizationInvitationHistoryItemDto['status']): string { if (status === 'PENDING') return 'Invitation pending'; if (status === 'ACTIVE') return 'Invitation Accepted'; if (status === 'REJECTED') return 'Invitation rejected'; return formatOrganizationStatusLabel(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 '—'; 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 [requestLinkRowId, setRequestLinkRowId] = useState(null); const [deleteLinkRowId, setDeleteLinkRowId] = useState(null); const [items, setItems] = useState([]); const [manualOrganizationName, setManualOrganizationName] = useState(''); const [manualOwnerEmail, setManualOwnerEmail] = useState(''); const [inviteLoading, setInviteLoading] = useState(false); const [copiedId, setCopiedId] = useState(null); const [pendingInviteLinks, setPendingInviteLinks] = useState>({}); const [showInviteForm, setShowInviteForm] = useState(false); const [historyOpen, setHistoryOpen] = useState(false); const [historyLoading, setHistoryLoading] = useState(false); const [historyItems, setHistoryItems] = useState([]); 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(() => { if (!currentOrganization?.id) return; setPendingInviteLinks(readStoredInviteLinks(currentOrganization.id)); }, [currentOrganization?.id]); 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 submitRequestLink(targetOrganizationId: string) { setRequestLinkRowId(targetOrganizationId); setError(''); try { await organizationApi.createLink(targetOrganizationId); setSuccess(`${counterpartLabel} link request sent`); setSearchResults([]); setQuery(''); setMode('existing'); await loadList(); } catch (e) { setError(formatApiMessage(e)); } finally { setRequestLinkRowId(null); } } async function sendInvite() { setInviteLoading(true); setError(''); try { const res = await organizationApi.invite({ organizationName: manualOrganizationName.trim(), ownerEmail: manualOwnerEmail.trim(), }); if (currentOrganization?.id) { const nextLinks = { ...pendingInviteLinks, [res.data.invitationId]: { invitationId: res.data.invitationId, ownerEmail: manualOwnerEmail.trim().toLowerCase(), invitationUrl: res.data.invitationUrl, }, }; setPendingInviteLinks(nextLinks); writeStoredInviteLinks(currentOrganization.id, nextLinks); } 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 openInvitationHistory() { setHistoryOpen(true); setHistoryLoading(true); setError(''); try { const res = await organizationApi.listInvitations(); setHistoryItems(res.data.items); } catch (e) { setError(formatApiMessage(e)); } finally { setHistoryLoading(false); } } async function respondToPendingLink(linkId: string, action: 'ACCEPT' | 'REJECT') { setRequestLinkRowId(linkId); setError(''); try { await organizationApi.respondLink(linkId, action); setSuccess(action === 'ACCEPT' ? 'Link request accepted' : 'Link request rejected'); await loadList(); } catch (e) { setError(formatApiMessage(e)); } finally { setRequestLinkRowId(null); } } async function deleteLinkedOrganization(linkId: string) { setDeleteLinkRowId(linkId); setError(''); try { await organizationApi.deleteLink(linkId); setSuccess('Linked organization removed'); await loadList(); } catch (e) { setError(formatApiMessage(e)); } finally { setDeleteLinkRowId(null); } } async function copyInvitationLink(invitationId: string) { setError(''); try { let invitationUrl = pendingInviteLinks[invitationId]?.invitationUrl; if (!invitationUrl) { const res = await organizationApi.getInvitationLink(invitationId); invitationUrl = res.data.invitationUrl; if (currentOrganization?.id) { const nextLinks = { ...pendingInviteLinks, [invitationId]: { invitationId, ownerEmail: historyItems.find((item) => item.id === invitationId)?.ownerEmail?.toLowerCase() ?? '', invitationUrl, }, }; setPendingInviteLinks(nextLinks); writeStoredInviteLinks(currentOrganization.id, nextLinks); } } await navigator.clipboard.writeText(invitationUrl); setCopiedId(invitationId); setTimeout(() => setCopiedId(null), 1500); } catch { setError('Could not copy invitation link'); } } function clearSearchView() { setMode('existing'); setQuery(''); setSearchResults([]); setShowInviteForm(false); } if (!currentOrganization) { return

Loading organization...

; } return (

{tabLabel}

Search organizations and send link requests or invitation links in one place.

{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; return ( ); }) ) ) : searchResults.length > 0 ? ( searchResults.map((r) => ( )) ) : ( )} } /> {historyOpen && (

Invitation History

{historyLoading ? (

Loading invitation history...

) : historyItems.length === 0 ? (

No invitations yet.

) : (
Organization Owner email Date Status Action
Loading...
No organizations linked or pending yet. Use search to find and connect.
{row.organizationName} {row.ownerEmail} {formatTableDate(row.createdAt)} {formatLinkStatusLabel(row.status)}
{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)} />
)}
} body={ <> {historyItems.map((inv) => ( ))} } /> )} )} ); }
Organization Owner email Date Status Action
{inv.organizationName} {inv.ownerEmail} {formatTableDate(inv.createdAt)} {formatInvitationStatusLabel(inv.status)} {inv.status === 'PENDING' ? ( ) : ( )}