Files
dyolink/frontend/src/components/ui/organizations/OrganizationConnectionsMobileList.tsx

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>
);
}