259 lines
9.9 KiB
TypeScript
259 lines
9.9 KiB
TypeScript
'use client';
|
|
|
|
import { Check, History, Trash2, UserPlus, X } from 'lucide-react';
|
|
import type {
|
|
CounterpartItemDto,
|
|
CounterpartSearchResultDto,
|
|
} from '@/lib/api/organization';
|
|
import { Badge, organizationConnectionStatusVariant } from '@/components/ui/shared/Badge';
|
|
import { Button } from '@/components/ui/shared/Button';
|
|
import { Card } from '@/components/ui/shared/Card';
|
|
import { Input } from '@/components/ui/shared/Input';
|
|
import { CopyInvitationLinkButton } from '@/components/ui/organizations/CopyInvitationLinkButton';
|
|
import type { InvitationLinkTarget } from '@/components/invitations/organizationInviteLinks';
|
|
|
|
type OrganizationConnectionsMobileListProps = {
|
|
loading: boolean;
|
|
mode: 'existing' | 'search';
|
|
existingRows: CounterpartItemDto[];
|
|
searchResults: CounterpartSearchResultDto[];
|
|
currentOrganizationId: string;
|
|
counterpart: string;
|
|
pendingConnectionRowId: string | null;
|
|
deleteConnectionRowId: string | null;
|
|
copiedId: string | null;
|
|
copyingInvitationId: string | null;
|
|
showInviteForm: boolean;
|
|
manualOrganizationName: string;
|
|
manualOwnerEmail: string;
|
|
inviteLoading: boolean;
|
|
formatConnectionStatusLabel: (row: CounterpartItemDto, orgId: string) => string;
|
|
formatTableDate: (value: string) => string;
|
|
getInvitationTarget: (row: CounterpartItemDto) => InvitationLinkTarget | null;
|
|
onCopyInvitation: (row: CounterpartItemDto) => void;
|
|
onRespond: (rowId: string, action: 'ACCEPT' | 'REJECT') => void;
|
|
onViewCaseHistory: (row: CounterpartItemDto) => void;
|
|
onDeleteConnection: (rowId: string) => void;
|
|
onSendConnectionRequest: (orgId: string) => void;
|
|
onToggleInviteForm: () => void;
|
|
onManualOrganizationNameChange: (value: string) => void;
|
|
onManualOwnerEmailChange: (value: string) => void;
|
|
onSendInvite: () => void;
|
|
labels: {
|
|
loading: string;
|
|
emptyConnections: string;
|
|
noDirectoryResults: string;
|
|
hideInvitationFields: string;
|
|
sendInvitationLink: string;
|
|
counterpartNameLabel: string;
|
|
ownerEmailLabel: string;
|
|
sendInvitation: string;
|
|
sendRequest: string;
|
|
acceptRequest: string;
|
|
declineRequest: string;
|
|
viewCaseHistory: string;
|
|
removeConnection: string;
|
|
statusToday: string;
|
|
statusFound: string;
|
|
};
|
|
};
|
|
|
|
export function OrganizationConnectionsMobileList({
|
|
loading,
|
|
mode,
|
|
existingRows,
|
|
searchResults,
|
|
currentOrganizationId,
|
|
counterpart,
|
|
pendingConnectionRowId,
|
|
deleteConnectionRowId,
|
|
copiedId,
|
|
copyingInvitationId,
|
|
showInviteForm,
|
|
manualOrganizationName,
|
|
manualOwnerEmail,
|
|
inviteLoading,
|
|
formatConnectionStatusLabel,
|
|
formatTableDate,
|
|
getInvitationTarget,
|
|
onCopyInvitation,
|
|
onRespond,
|
|
onViewCaseHistory,
|
|
onDeleteConnection,
|
|
onSendConnectionRequest,
|
|
onToggleInviteForm,
|
|
onManualOrganizationNameChange,
|
|
onManualOwnerEmailChange,
|
|
onSendInvite,
|
|
labels,
|
|
}: OrganizationConnectionsMobileListProps) {
|
|
if (loading) {
|
|
return <p className="text-sm text-text-secondary lg:hidden">{labels.loading}</p>;
|
|
}
|
|
|
|
if (mode === 'existing') {
|
|
if (existingRows.length === 0) {
|
|
return (
|
|
<p className="text-sm text-text-secondary lg:hidden surface-card p-4">
|
|
{labels.emptyConnections}
|
|
</p>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<ul className="space-y-3 lg:hidden">
|
|
{existingRows.map((row) => {
|
|
const canRespond =
|
|
row.status === 'PENDING' &&
|
|
row.requestedByOrganizationId !== null &&
|
|
row.requestedByOrganizationId !== currentOrganizationId;
|
|
const invitationTarget = getInvitationTarget(row);
|
|
|
|
return (
|
|
<li key={row.id}>
|
|
<Card padding="sm" className="space-y-3">
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div className="min-w-0">
|
|
<p className="font-medium text-text-primary truncate">{row.organizationName}</p>
|
|
<p className="text-sm text-text-secondary truncate mt-0.5">{row.ownerEmail}</p>
|
|
<p className="text-xs text-text-muted mt-1">{formatTableDate(row.createdAt)}</p>
|
|
</div>
|
|
<Badge variant={organizationConnectionStatusVariant(row.status)} fixedWidth={false}>
|
|
{formatConnectionStatusLabel(row, currentOrganizationId)}
|
|
</Badge>
|
|
</div>
|
|
|
|
<div className="flex flex-wrap items-center gap-1.5 pt-1 border-t border-border/60">
|
|
{invitationTarget ? (
|
|
<CopyInvitationLinkButton
|
|
invitation={invitationTarget}
|
|
copied={copiedId === invitationTarget.id}
|
|
copying={copyingInvitationId === invitationTarget.id}
|
|
onCopy={() => onCopyInvitation(row)}
|
|
/>
|
|
) : null}
|
|
{canRespond ? (
|
|
<>
|
|
<button
|
|
type="button"
|
|
className="p-2 rounded-md text-text-secondary hover:bg-background-card/80 hover:text-text-primary disabled:opacity-50"
|
|
disabled={pendingConnectionRowId !== null && pendingConnectionRowId !== row.id}
|
|
onClick={() => onRespond(row.id, 'ACCEPT')}
|
|
aria-label={labels.acceptRequest}
|
|
title={labels.acceptRequest}
|
|
>
|
|
<Check className="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="p-2 rounded-md text-text-secondary hover:bg-red-500/15 hover:text-red-600 disabled:opacity-50"
|
|
disabled={pendingConnectionRowId !== null && pendingConnectionRowId !== row.id}
|
|
onClick={() => onRespond(row.id, 'REJECT')}
|
|
aria-label={labels.declineRequest}
|
|
title={labels.declineRequest}
|
|
>
|
|
<X className="w-4 h-4" />
|
|
</button>
|
|
</>
|
|
) : null}
|
|
{row.status === 'ACTIVE' ? (
|
|
<>
|
|
<button
|
|
type="button"
|
|
className="p-2 rounded-md text-text-secondary hover:bg-background-card/80 hover:text-text-primary"
|
|
onClick={() => onViewCaseHistory(row)}
|
|
aria-label={labels.viewCaseHistory}
|
|
title={labels.viewCaseHistory}
|
|
>
|
|
<History className="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="p-2 rounded-md text-text-secondary hover:bg-red-500/15 hover:text-red-600 disabled:opacity-50"
|
|
disabled={deleteConnectionRowId !== null && deleteConnectionRowId !== row.id}
|
|
onClick={() => onDeleteConnection(row.id)}
|
|
aria-label={labels.removeConnection}
|
|
title={labels.removeConnection}
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</button>
|
|
</>
|
|
) : null}
|
|
</div>
|
|
</Card>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
);
|
|
}
|
|
|
|
if (searchResults.length > 0) {
|
|
return (
|
|
<ul className="space-y-3 lg:hidden">
|
|
{searchResults.map((result) => (
|
|
<li key={result.id}>
|
|
<Card padding="sm" className="space-y-3">
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div className="min-w-0">
|
|
<p className="font-medium text-text-primary truncate">{result.name}</p>
|
|
<p className="text-sm text-text-secondary truncate mt-0.5">{result.owner.email}</p>
|
|
<p className="text-xs text-text-muted mt-1">{labels.statusToday}</p>
|
|
</div>
|
|
<Badge variant="default" fixedWidth={false}>
|
|
{labels.statusFound}
|
|
</Badge>
|
|
</div>
|
|
<div className="flex justify-end pt-1 border-t border-border/60">
|
|
<button
|
|
type="button"
|
|
className="p-2 rounded-md text-text-secondary hover:bg-background-card/80 hover:text-text-primary disabled:opacity-50"
|
|
disabled={pendingConnectionRowId !== null && pendingConnectionRowId !== result.id}
|
|
onClick={() => onSendConnectionRequest(result.id)}
|
|
aria-label={labels.sendRequest}
|
|
title={labels.sendRequest}
|
|
>
|
|
<UserPlus className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</Card>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="lg:hidden surface-card p-4 space-y-3">
|
|
<p className="text-sm text-text-secondary">{labels.noDirectoryResults}</p>
|
|
<Button type="button" onClick={onToggleInviteForm} className="w-full sm:w-auto">
|
|
{showInviteForm ? labels.hideInvitationFields : labels.sendInvitationLink}
|
|
</Button>
|
|
{showInviteForm ? (
|
|
<div className="grid gap-3">
|
|
<Input
|
|
label={labels.counterpartNameLabel}
|
|
value={manualOrganizationName}
|
|
onChange={(e) => onManualOrganizationNameChange(e.target.value)}
|
|
/>
|
|
<Input
|
|
label={labels.ownerEmailLabel}
|
|
type="email"
|
|
value={manualOwnerEmail}
|
|
onChange={(e) => onManualOwnerEmailChange(e.target.value)}
|
|
/>
|
|
<Button
|
|
type="button"
|
|
isLoading={inviteLoading}
|
|
disabled={!manualOrganizationName.trim() || !manualOwnerEmail.trim()}
|
|
onClick={onSendInvite}
|
|
className="w-full"
|
|
>
|
|
{labels.sendInvitation}
|
|
</Button>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|