127 lines
4.9 KiB
TypeScript
127 lines
4.9 KiB
TypeScript
'use client';
|
|
|
|
import { useTranslations } from 'next-intl';
|
|
import { DialogCloseButton } from '@/components/ui/shared/DialogCloseButton';
|
|
import { ToastStack, type ToastMessages } from '@/components/ui/shared/Toast';
|
|
import type { OrganizationInvitationHistoryItemDto } from '@/lib/api/organization';
|
|
import { Badge, organizationConnectionStatusVariant } from '@/components/ui/shared/Badge';
|
|
import { Table } from '@/components/ui/shared/Table';
|
|
import { CopyInvitationLinkButton } from '@/components/ui/organizations/CopyInvitationLinkButton';
|
|
|
|
function formatTableDate(value: string): string {
|
|
const d = new Date(value);
|
|
if (Number.isNaN(d.getTime())) return '—';
|
|
return d.toLocaleDateString();
|
|
}
|
|
|
|
type InvitationHistoryDialogProps = {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
loading: boolean;
|
|
items: OrganizationInvitationHistoryItemDto[];
|
|
copiedId: string | null;
|
|
copyingInvitationId: string | null;
|
|
onCopy: (invitation: OrganizationInvitationHistoryItemDto) => void;
|
|
/** Same page-level toasts, rendered at top of dialog while it is open. */
|
|
toastMessages?: ToastMessages;
|
|
};
|
|
|
|
export function InvitationHistoryDialog({
|
|
open,
|
|
onClose,
|
|
loading,
|
|
items,
|
|
copiedId,
|
|
copyingInvitationId,
|
|
onCopy,
|
|
toastMessages,
|
|
}: InvitationHistoryDialogProps) {
|
|
const t = useTranslations('organizations');
|
|
|
|
function formatInvitationStatusLabel(
|
|
status: OrganizationInvitationHistoryItemDto['status'],
|
|
): string {
|
|
if (status === 'PENDING') return t('statusPending');
|
|
if (status === 'ACTIVE') return t('statusAccepted');
|
|
if (status === 'REJECTED') return t('statusRejected');
|
|
if (status === 'EXPIRED') return t('statusExpired');
|
|
return status;
|
|
}
|
|
|
|
if (!open) return null;
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50">
|
|
<div
|
|
className="w-full max-w-[min(56rem,calc(100vw-17rem))] max-h-[90vh] overflow-y-auto rounded-[var(--radius-md)] border border-border bg-background-secondary p-6 shadow-xl space-y-4"
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-labelledby="invitation-history-title"
|
|
>
|
|
<div className="flex items-start justify-between gap-3">
|
|
<h2 id="invitation-history-title" className="text-lg font-semibold text-text-primary pr-2">
|
|
{t('historyTitle')}
|
|
</h2>
|
|
<DialogCloseButton onClick={onClose} />
|
|
</div>
|
|
|
|
{toastMessages && <ToastStack {...toastMessages} />}
|
|
|
|
{loading ? (
|
|
<p className="text-sm text-text-secondary">{t('loadingHistory')}</p>
|
|
) : items.length === 0 ? (
|
|
<p className="text-sm text-text-secondary">{t('historyEmpty')}</p>
|
|
) : (
|
|
<Table
|
|
headers={
|
|
<tr>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-text-muted uppercase tracking-wider">
|
|
{t('tableOrganization')}
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-text-muted uppercase tracking-wider">
|
|
{t('tableOwnerEmail')}
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-text-muted uppercase tracking-wider">
|
|
{t('tableDate')}
|
|
</th>
|
|
<th className="px-6 py-3 text-center text-xs font-medium text-text-muted uppercase tracking-wider">
|
|
{t('tableStatus')}
|
|
</th>
|
|
<th className="px-6 py-3 text-right text-xs font-medium text-text-muted uppercase tracking-wider">
|
|
{t('tableInvitationLink')}
|
|
</th>
|
|
</tr>
|
|
}
|
|
body={
|
|
<>
|
|
{items.map((inv) => (
|
|
<tr key={inv.id} className="hover:bg-background-secondary/45">
|
|
<td className="px-6 py-1.5 text-sm text-text-primary">{inv.organizationName}</td>
|
|
<td className="px-6 py-1.5 text-sm text-text-secondary">{inv.ownerEmail}</td>
|
|
<td className="px-6 py-1.5 text-sm text-text-secondary">
|
|
{formatTableDate(inv.createdAt)}
|
|
</td>
|
|
<td className="px-6 py-1.5 text-center align-middle">
|
|
<Badge variant={organizationConnectionStatusVariant(inv.status)} fixedWidth={false}>
|
|
{formatInvitationStatusLabel(inv.status)}
|
|
</Badge>
|
|
</td>
|
|
<td className="px-6 py-1.5 text-right align-middle">
|
|
<CopyInvitationLinkButton
|
|
invitation={inv}
|
|
copied={copiedId === inv.id}
|
|
copying={copyingInvitationId === inv.id}
|
|
onCopy={() => onCopy(inv)}
|
|
/>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</>
|
|
}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|