Files
medreport_mrb2b/packages/features/team-accounts/src/components/members/account-members-table.tsx
2025-10-09 18:59:03 +03:00

363 lines
9.9 KiB
TypeScript

'use client';
import { useMemo, useState } from 'react';
import { ColumnDef } from '@tanstack/react-table';
import { Ellipsis } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { formatCurrency } from '@kit/shared/utils';
import { Database } from '@kit/supabase/database';
import { Badge } from '@kit/ui/badge';
import { Button } from '@kit/ui/button';
import { DataTable } from '@kit/ui/data-table';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@kit/ui/dropdown-menu';
import { If } from '@kit/ui/if';
import { Input } from '@kit/ui/input';
import { ProfileAvatar } from '@kit/ui/profile-avatar';
import { Trans } from '@kit/ui/trans';
import { RemoveMemberDialog } from './remove-member-dialog';
import { RoleBadge } from './role-badge';
import { TransferOwnershipDialog } from './transfer-ownership-dialog';
import UpdateEmployeeBenefitDialog from './update-employee-benefit-dialog';
import { UpdateMemberRoleDialog } from './update-member-role-dialog';
type Members =
Database['medreport']['Functions']['get_account_members']['Returns'];
interface Permissions {
canUpdateRole: boolean;
canRemoveFromAccount: (roleHierarchy: number) => boolean;
canTransferOwnership: boolean;
canUpdateBenefit: boolean;
}
type AccountMembersTableProps = {
members: Members;
currentUserId: string;
currentAccountId: string;
userRoleHierarchy: number;
isPrimaryOwner: boolean;
canManageRoles: boolean;
canUpdateBenefit: boolean;
membersBenefitsUsage: {
personal_account_id: string;
benefit_amount: number;
benefit_unused_amount: number;
}[];
};
export function AccountMembersTable({
members,
currentUserId,
currentAccountId,
isPrimaryOwner,
userRoleHierarchy,
canManageRoles,
canUpdateBenefit,
membersBenefitsUsage,
}: AccountMembersTableProps) {
const [search, setSearch] = useState('');
const { t } = useTranslation('teams');
const permissions = {
canUpdateRole: canManageRoles,
canRemoveFromAccount: (targetRole: number) => {
return (
isPrimaryOwner || (canManageRoles && userRoleHierarchy < targetRole)
);
},
canTransferOwnership: isPrimaryOwner,
canUpdateBenefit,
};
const columns = useGetColumns(permissions, {
currentUserId,
currentAccountId,
currentRoleHierarchy: userRoleHierarchy,
membersBenefitsUsage,
});
const searchString = search.toLowerCase();
const filteredMembers =
searchString.length > 0
? members.filter((member) => {
const displayName = (
member.name ??
member.email.split('@')[0] ??
''
).toLowerCase();
return (
displayName.includes(searchString) ||
member.role.toLowerCase().includes(searchString) ||
(member.personal_code || '').includes(searchString)
);
})
: members;
return (
<div className={'flex flex-col space-y-2'}>
<Input
value={search}
onInput={(e) => setSearch((e.target as HTMLInputElement).value)}
placeholder={t(`searchMembersPlaceholder`)}
/>
<DataTable columns={columns} data={filteredMembers} />
</div>
);
}
function useGetColumns(
permissions: Permissions,
params: {
currentUserId: string;
currentAccountId: string;
currentRoleHierarchy: number;
membersBenefitsUsage: {
personal_account_id: string;
benefit_amount: number;
benefit_unused_amount: number;
}[];
},
): ColumnDef<Members[0]>[] {
const {
t,
i18n: { language },
} = useTranslation('teams');
return useMemo(
() => [
{
header: t('memberName'),
size: 200,
cell: ({ row }) => {
const member = row.original;
const displayName = member.name ?? member.email.split('@')[0];
const isSelf = member.user_id === params.currentUserId;
return (
<span className={'flex items-center space-x-4 text-left'}>
<span>
<ProfileAvatar
displayName={displayName}
pictureUrl={member.picture_url}
/>
</span>
<span>{displayName}</span>
<If condition={isSelf}>
<Badge variant={'outline'}>{t('youLabel')}</Badge>
</If>
</span>
);
},
},
{
header: t('emailLabel'),
accessorKey: 'email',
cell: ({ row }) => {
return row.original.email ?? '-';
},
},
{
header: t('personalCode'),
accessorKey: 'personal_code',
cell: ({ row }) => {
return row.original.personal_code ?? '-';
},
},
{
header: t('distributedBenefitsAmount'),
cell: ({ row }) => {
let benefitAmount = params.membersBenefitsUsage.find(
(usage) => usage.personal_account_id === row.original.id,
)?.benefit_amount;
if (typeof benefitAmount !== 'number') {
benefitAmount = 0;
}
return formatCurrency({
currencyCode: 'EUR',
locale: language,
value: benefitAmount,
});
},
},
{
header: t('distributedBenefitsUnusedAmount'),
cell: ({ row }) => {
let benefitUnusedAmount = params.membersBenefitsUsage.find(
(usage) => usage.personal_account_id === row.original.id,
)?.benefit_unused_amount;
if (typeof benefitUnusedAmount !== 'number') {
benefitUnusedAmount = 0;
}
return formatCurrency({
currencyCode: 'EUR',
locale: language,
value: benefitUnusedAmount,
});
},
},
{
header: t('roleLabel'),
cell: ({ row }) => {
const { role } = row.original;
return (
<span
className={
'flex flex-wrap items-center space-y-1 space-x-1 sm:flex-nowrap sm:space-y-0'
}
>
<RoleBadge role={role} />
</span>
);
},
},
{
header: t('joinedAtLabel'),
cell: ({ row }) => {
return new Date(row.original.created_at).toLocaleDateString();
},
},
{
header: '',
id: 'actions',
cell: ({ row }) => (
<ActionsDropdown
permissions={permissions}
member={row.original}
currentTeamAccountId={params.currentAccountId}
currentRoleHierarchy={params.currentRoleHierarchy}
/>
),
},
],
[t, params, permissions, language],
);
}
function ActionsDropdown({
permissions,
member,
currentTeamAccountId,
currentRoleHierarchy,
}: {
permissions: Permissions;
member: Members[0];
currentTeamAccountId: string;
currentRoleHierarchy: number;
}) {
const [isRemoving, setIsRemoving] = useState(false);
const [isTransferring, setIsTransferring] = useState(false);
const [isUpdatingRole, setIsUpdatingRole] = useState(false);
const [isUpdatingBenefit, setIsUpdatingBenefit] = useState(false);
const isPrimaryOwner = member.primary_owner_user_id === member.user_id;
const memberRoleHierarchy = member.role_hierarchy_level;
const canRemoveFromAccount =
permissions.canRemoveFromAccount(memberRoleHierarchy);
// if has no permission to update role, transfer ownership or remove from account
// do not render the dropdown menu
if (
!permissions.canUpdateRole &&
!permissions.canTransferOwnership &&
!canRemoveFromAccount &&
!permissions.canUpdateBenefit
) {
return null;
}
return (
<>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant={'ghost'} size={'icon'}>
<Ellipsis className={'h-5 w-5'} />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<If condition={permissions.canUpdateRole}>
<DropdownMenuItem onClick={() => setIsUpdatingRole(true)}>
<Trans i18nKey={'teams:updateRole'} />
</DropdownMenuItem>
</If>
<If condition={permissions.canTransferOwnership && !isPrimaryOwner}>
<DropdownMenuItem onClick={() => setIsTransferring(true)}>
<Trans i18nKey={'teams:transferOwnership'} />
</DropdownMenuItem>
</If>
<If condition={canRemoveFromAccount && !isPrimaryOwner}>
<DropdownMenuItem onClick={() => setIsRemoving(true)}>
<Trans i18nKey={'teams:removeMember'} />
</DropdownMenuItem>
</If>
<If condition={permissions.canUpdateBenefit}>
<DropdownMenuItem onClick={() => setIsUpdatingBenefit(true)}>
<Trans i18nKey={'teams:updateBenefit'} />
</DropdownMenuItem>
</If>
</DropdownMenuContent>
</DropdownMenu>
<If condition={isRemoving}>
<RemoveMemberDialog
isOpen
setIsOpen={setIsRemoving}
teamAccountId={currentTeamAccountId}
userId={member.user_id}
/>
</If>
<If condition={isUpdatingRole}>
<UpdateMemberRoleDialog
isOpen
setIsOpen={setIsUpdatingRole}
userId={member.user_id}
userRole={member.role}
teamAccountId={currentTeamAccountId}
userRoleHierarchy={currentRoleHierarchy}
/>
</If>
<If condition={isTransferring}>
<TransferOwnershipDialog
isOpen
setIsOpen={setIsTransferring}
targetDisplayName={member.name ?? member.email}
accountId={member.account_id}
userId={member.user_id}
/>
</If>
<If condition={isUpdatingBenefit}>
<UpdateEmployeeBenefitDialog
isOpen
setIsOpen={setIsUpdatingBenefit}
accountId={member.account_id}
userId={member.user_id}
isEligibleForBenefits={member.is_eligible_for_benefits}
/>
</If>
</>
);
}