191 lines
6.9 KiB
TypeScript
191 lines
6.9 KiB
TypeScript
|
|
'use client';
|
||
|
|
|
||
|
|
import { Check, Copy, Pencil, Trash2, UserCheck, UserX } from 'lucide-react';
|
||
|
|
import type { StaffMemberDto } from '@/lib/api/staff';
|
||
|
|
import { Badge } from '@/components/ui/shared/Badge';
|
||
|
|
import { Card } from '@/components/ui/shared/Card';
|
||
|
|
|
||
|
|
type StaffMembersMobileListProps = {
|
||
|
|
members: StaffMemberDto[];
|
||
|
|
canEdit: boolean;
|
||
|
|
organizationType: string | undefined;
|
||
|
|
copiedInviteMembershipId: string | null;
|
||
|
|
copyingInviteMembershipId: string | null;
|
||
|
|
enablingMembershipId: string | null;
|
||
|
|
disablingMembershipId: string | null;
|
||
|
|
formatAccessSummary: (member: StaffMemberDto) => string;
|
||
|
|
canShareInviteLink: (member: StaffMemberDto) => boolean;
|
||
|
|
canEnable: (member: StaffMemberDto) => boolean;
|
||
|
|
canDisable: (member: StaffMemberDto) => boolean;
|
||
|
|
onCopyInviteLink: (member: StaffMemberDto) => void;
|
||
|
|
onEnable: (member: StaffMemberDto) => void;
|
||
|
|
onDisable: (member: StaffMemberDto) => void;
|
||
|
|
onEdit: (member: StaffMemberDto) => void;
|
||
|
|
onDelete: (member: StaffMemberDto) => void;
|
||
|
|
labels: {
|
||
|
|
roleOwner: string;
|
||
|
|
roleStaff: string;
|
||
|
|
statusActive: string;
|
||
|
|
statusPending: string;
|
||
|
|
statusDisabled: string;
|
||
|
|
statusExpired: string;
|
||
|
|
allFeatures: string;
|
||
|
|
copyInviteLink: string;
|
||
|
|
enableMemberTitle: string;
|
||
|
|
disableMemberTitle: string;
|
||
|
|
editMemberAria: string;
|
||
|
|
deleteMemberAria: string;
|
||
|
|
};
|
||
|
|
};
|
||
|
|
|
||
|
|
function memberStatusBadge(
|
||
|
|
member: StaffMemberDto,
|
||
|
|
labels: StaffMembersMobileListProps['labels'],
|
||
|
|
) {
|
||
|
|
if (member.isOwner || member.invitationStatus === 'ACTIVE') {
|
||
|
|
return <Badge variant="success">{labels.statusActive}</Badge>;
|
||
|
|
}
|
||
|
|
if (member.invitationStatus === 'PENDING') {
|
||
|
|
return <Badge variant="warning">{labels.statusPending}</Badge>;
|
||
|
|
}
|
||
|
|
if (member.invitationStatus === 'DISABLED') {
|
||
|
|
return <Badge variant="default">{labels.statusDisabled}</Badge>;
|
||
|
|
}
|
||
|
|
return <Badge variant="danger">{labels.statusExpired}</Badge>;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function StaffMembersMobileList({
|
||
|
|
members,
|
||
|
|
canEdit,
|
||
|
|
copiedInviteMembershipId,
|
||
|
|
copyingInviteMembershipId,
|
||
|
|
enablingMembershipId,
|
||
|
|
disablingMembershipId,
|
||
|
|
formatAccessSummary,
|
||
|
|
canShareInviteLink,
|
||
|
|
canEnable,
|
||
|
|
canDisable,
|
||
|
|
onCopyInviteLink,
|
||
|
|
onEnable,
|
||
|
|
onDisable,
|
||
|
|
onEdit,
|
||
|
|
onDelete,
|
||
|
|
labels,
|
||
|
|
}: StaffMembersMobileListProps) {
|
||
|
|
return (
|
||
|
|
<ul className="space-y-3 lg:hidden">
|
||
|
|
{members.map((member) => (
|
||
|
|
<li key={member.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">{member.name}</p>
|
||
|
|
<p className="text-sm text-text-secondary truncate mt-0.5">{member.email}</p>
|
||
|
|
<p className="text-xs text-text-muted mt-1">
|
||
|
|
{member.isOwner ? labels.roleOwner : labels.roleStaff}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
{memberStatusBadge(member, labels)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<p className="text-sm text-text-secondary line-clamp-3">
|
||
|
|
{member.isOwner ? labels.allFeatures : formatAccessSummary(member)}
|
||
|
|
</p>
|
||
|
|
|
||
|
|
{!member.isOwner ? (
|
||
|
|
<div className="flex flex-wrap items-center gap-1.5 pt-1 border-t border-border/60">
|
||
|
|
{canShareInviteLink(member) ? (
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
className="p-2 rounded-md text-text-secondary hover:bg-background-card/80 hover:text-text-primary disabled:opacity-50"
|
||
|
|
disabled={copyingInviteMembershipId === member.id}
|
||
|
|
onClick={() => onCopyInviteLink(member)}
|
||
|
|
aria-label={labels.copyInviteLink}
|
||
|
|
title={labels.copyInviteLink}
|
||
|
|
>
|
||
|
|
{copiedInviteMembershipId === member.id ? (
|
||
|
|
<Check className="w-4 h-4" />
|
||
|
|
) : (
|
||
|
|
<Copy className="w-4 h-4" />
|
||
|
|
)}
|
||
|
|
</button>
|
||
|
|
) : null}
|
||
|
|
{canEnable(member) ? (
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
className={`p-2 rounded-md ${
|
||
|
|
canEdit
|
||
|
|
? 'text-text-secondary hover:bg-background-card/80 hover:text-primary'
|
||
|
|
: 'text-text-muted opacity-50 cursor-not-allowed'
|
||
|
|
}`}
|
||
|
|
aria-label={labels.enableMemberTitle}
|
||
|
|
disabled={!canEdit || enablingMembershipId === member.id}
|
||
|
|
title={labels.enableMemberTitle}
|
||
|
|
onClick={() => {
|
||
|
|
if (!canEdit) return;
|
||
|
|
onEnable(member);
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<UserCheck className="w-4 h-4" />
|
||
|
|
</button>
|
||
|
|
) : null}
|
||
|
|
{canDisable(member) ? (
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
className={`p-2 rounded-md ${
|
||
|
|
canEdit
|
||
|
|
? 'text-text-secondary hover:bg-background-card/80 hover:text-amber-600'
|
||
|
|
: 'text-text-muted opacity-50 cursor-not-allowed'
|
||
|
|
}`}
|
||
|
|
aria-label={labels.disableMemberTitle}
|
||
|
|
disabled={!canEdit || disablingMembershipId === member.id}
|
||
|
|
title={labels.disableMemberTitle}
|
||
|
|
onClick={() => {
|
||
|
|
if (!canEdit) return;
|
||
|
|
onDisable(member);
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<UserX className="w-4 h-4" />
|
||
|
|
</button>
|
||
|
|
) : null}
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
className={`p-2 rounded-md ${
|
||
|
|
canEdit
|
||
|
|
? 'text-text-secondary hover:bg-background-card/80 hover:text-text-primary'
|
||
|
|
: 'text-text-muted opacity-50 cursor-not-allowed'
|
||
|
|
}`}
|
||
|
|
aria-label={labels.editMemberAria}
|
||
|
|
disabled={!canEdit}
|
||
|
|
onClick={() => {
|
||
|
|
if (!canEdit) return;
|
||
|
|
onEdit(member);
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<Pencil className="w-4 h-4" />
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
className={`p-2 rounded-md ${
|
||
|
|
canEdit
|
||
|
|
? 'text-text-secondary hover:bg-red-500/15 hover:text-red-600'
|
||
|
|
: 'text-text-muted opacity-50 cursor-not-allowed'
|
||
|
|
}`}
|
||
|
|
aria-label={labels.deleteMemberAria}
|
||
|
|
disabled={!canEdit}
|
||
|
|
onClick={() => {
|
||
|
|
if (!canEdit) return;
|
||
|
|
onDelete(member);
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<Trash2 className="w-4 h-4" />
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
) : null}
|
||
|
|
</Card>
|
||
|
|
</li>
|
||
|
|
))}
|
||
|
|
</ul>
|
||
|
|
);
|
||
|
|
}
|