2026-05-17 00:02:13 +03:30
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
import { Check, Copy } from 'lucide-react';
|
2026-05-17 01:39:49 +03:30
|
|
|
import { DialogCloseButton } from '@/components/ui/common/DialogCloseButton';
|
2026-05-17 00:02:13 +03:30
|
|
|
import type { OrganizationInvitationHistoryItemDto } from '@/lib/api/organization';
|
|
|
|
|
import { canShareOrganizationInviteLink } from '@/components/invitations/organizationInviteLinks';
|
|
|
|
|
import { Badge, organizationLinkStatusVariant } from '@/components/ui/common/Badge';
|
|
|
|
|
import { Table } from '@/components/ui/common/Table';
|
|
|
|
|
|
|
|
|
|
function formatInvitationStatusLabel(status: OrganizationInvitationHistoryItemDto['status']): string {
|
|
|
|
|
if (status === 'PENDING') return 'Invitation pending';
|
|
|
|
|
if (status === 'ACTIVE') return 'Invitation Accepted';
|
|
|
|
|
if (status === 'REJECTED') return 'Invitation rejected';
|
|
|
|
|
if (status === 'EXPIRED') return 'Invitation expired';
|
|
|
|
|
return status;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
copyError?: string;
|
|
|
|
|
copySuccess?: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export function InvitationHistoryDialog({
|
|
|
|
|
open,
|
|
|
|
|
onClose,
|
|
|
|
|
loading,
|
|
|
|
|
items,
|
|
|
|
|
copiedId,
|
|
|
|
|
copyingInvitationId,
|
|
|
|
|
onCopy,
|
|
|
|
|
copyError,
|
|
|
|
|
copySuccess,
|
|
|
|
|
}: InvitationHistoryDialogProps) {
|
|
|
|
|
if (!open) return null;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50">
|
|
|
|
|
<div
|
2026-05-17 01:39:49 +03:30
|
|
|
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"
|
2026-05-17 00:02:13 +03:30
|
|
|
role="dialog"
|
|
|
|
|
aria-modal="true"
|
|
|
|
|
aria-labelledby="invitation-history-title"
|
|
|
|
|
>
|
2026-05-17 01:39:49 +03:30
|
|
|
<div className="flex items-start justify-between gap-3">
|
|
|
|
|
<h2 id="invitation-history-title" className="text-lg font-semibold text-text-primary pr-2">
|
2026-05-17 00:02:13 +03:30
|
|
|
Invitation History
|
|
|
|
|
</h2>
|
2026-05-17 01:39:49 +03:30
|
|
|
<DialogCloseButton onClick={onClose} />
|
2026-05-17 00:02:13 +03:30
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{copyError && (
|
|
|
|
|
<div className="rounded-[var(--radius-md)] border border-red-500/40 bg-red-500/10 px-4 py-3 text-sm text-red-700 dark:text-red-300">
|
|
|
|
|
{copyError}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{copySuccess && (
|
|
|
|
|
<div className="rounded-[var(--radius-md)] border border-primary/30 bg-primary-soft/40 px-4 py-3 text-sm text-text-primary">
|
|
|
|
|
{copySuccess}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{loading ? (
|
|
|
|
|
<p className="text-sm text-text-secondary">Loading invitation history...</p>
|
|
|
|
|
) : items.length === 0 ? (
|
|
|
|
|
<p className="text-sm text-text-secondary">No invitations yet.</p>
|
|
|
|
|
) : (
|
|
|
|
|
<Table
|
|
|
|
|
headers={
|
|
|
|
|
<tr>
|
|
|
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-text-muted uppercase tracking-wider">
|
|
|
|
|
Organization
|
|
|
|
|
</th>
|
|
|
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-text-muted uppercase tracking-wider">
|
|
|
|
|
Owner email
|
|
|
|
|
</th>
|
|
|
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-text-muted uppercase tracking-wider">
|
|
|
|
|
Date
|
|
|
|
|
</th>
|
|
|
|
|
<th className="px-6 py-3 text-center text-xs font-medium text-text-muted uppercase tracking-wider">
|
|
|
|
|
Status
|
|
|
|
|
</th>
|
|
|
|
|
<th className="px-6 py-3 text-right text-xs font-medium text-text-muted uppercase tracking-wider">
|
|
|
|
|
Action
|
|
|
|
|
</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={organizationLinkStatusVariant(inv.status)} fixedWidth={false}>
|
|
|
|
|
{formatInvitationStatusLabel(inv.status)}
|
|
|
|
|
</Badge>
|
|
|
|
|
</td>
|
|
|
|
|
<td className="px-6 py-1.5 text-right align-middle">
|
|
|
|
|
{canShareOrganizationInviteLink(inv) ? (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="p-2 rounded-md text-text-secondary hover:bg-background-card/80 hover:text-text-primary disabled:opacity-50"
|
|
|
|
|
disabled={copyingInvitationId === inv.id}
|
|
|
|
|
onClick={() => onCopy(inv)}
|
|
|
|
|
aria-label="Copy invitation link"
|
|
|
|
|
title="Copy invitation link (generates a new link if needed)"
|
|
|
|
|
>
|
|
|
|
|
{copiedId === inv.id ? (
|
|
|
|
|
<Check className="w-4 h-4" />
|
|
|
|
|
) : (
|
|
|
|
|
<Copy className="w-4 h-4" />
|
|
|
|
|
)}
|
|
|
|
|
</button>
|
|
|
|
|
) : (
|
|
|
|
|
<span className="text-xs text-text-muted">—</span>
|
|
|
|
|
)}
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
))}
|
|
|
|
|
</>
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|