Files
medreport_mrb2b/packages/features/admin/src/components/admin-account-page.tsx

404 lines
11 KiB
TypeScript

import { BadgeX, Ban, ShieldPlus, VenetianMask } from 'lucide-react';
import { Tables } from '@kit/supabase/database';
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
import { Badge } from '@kit/ui/badge';
import { Button } from '@kit/ui/button';
import { Heading } from '@kit/ui/heading';
import { If } from '@kit/ui/if';
import { PageBody, PageHeader } from '@kit/ui/page';
import { ProfileAvatar } from '@kit/ui/profile-avatar';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@kit/ui/table';
import { AdminBanUserDialog } from './admin-ban-user-dialog';
import { AdminDeleteAccountDialog } from './admin-delete-account-dialog';
import { AdminDeleteUserDialog } from './admin-delete-user-dialog';
import { AdminImpersonateUserDialog } from './admin-impersonate-user-dialog';
import { AdminMembersTable } from './admin-members-table';
import { AdminMembershipsTable } from './admin-memberships-table';
import { AdminReactivateUserDialog } from './admin-reactivate-user-dialog';
type Account = Tables<'accounts'>;
type Membership = Tables<'accounts_memberships'>;
export function AdminAccountPage(props: {
account: Account & { memberships: Membership[] };
}) {
const isPersonalAccount = props.account.is_personal_account;
if (isPersonalAccount) {
return <PersonalAccountPage account={props.account} />;
}
return <TeamAccountPage account={props.account} />;
}
async function PersonalAccountPage(props: { account: Account }) {
const adminClient = getSupabaseServerAdminClient();
const { data, error } = await adminClient.auth.admin.getUserById(
props.account.id,
);
if (!data || error) {
throw new Error(`User not found`);
}
const memberships = await getMemberships(props.account.id);
const isBanned =
'banned_until' in data.user && data.user.banned_until !== 'none';
return (
<>
<PageHeader
className="border-b"
description={
<AppBreadcrumbs
values={{
[props.account.id]:
props.account.name ?? props.account.email ?? 'Account',
}}
/>
}
>
<div className={'flex gap-x-2.5'}>
<If condition={isBanned}>
<AdminReactivateUserDialog userId={props.account.id}>
<Button
size={'sm'}
variant={'secondary'}
data-test={'admin-reactivate-account-button'}
>
<ShieldPlus className={'mr-1 h-4'} />
Reactivate
</Button>
</AdminReactivateUserDialog>
</If>
<If condition={!isBanned}>
<AdminBanUserDialog userId={props.account.id}>
<Button
size={'sm'}
variant={'secondary'}
data-test={'admin-ban-account-button'}
>
<Ban className={'text-destructive mr-1 h-3'} />
Ban
</Button>
</AdminBanUserDialog>
<AdminImpersonateUserDialog userId={props.account.id}>
<Button
size={'sm'}
variant={'secondary'}
data-test={'admin-impersonate-button'}
>
<VenetianMask className={'mr-1 h-4 text-blue-500'} />
Impersonate
</Button>
</AdminImpersonateUserDialog>
</If>
<AdminDeleteUserDialog userId={props.account.id}>
<Button
size={'sm'}
variant={'destructive'}
data-test={'admin-delete-account-button'}
>
<BadgeX className={'mr-1 h-4'} />
Delete
</Button>
</AdminDeleteUserDialog>
</div>
</PageHeader>
<PageBody className={'space-y-6 py-4'}>
<div className={'flex items-center justify-between'}>
<div className={'flex items-center gap-x-4'}>
<div className={'flex items-center gap-x-2.5'}>
<ProfileAvatar
pictureUrl={props.account.picture_url}
displayName={props.account.name}
/>
<span className={'text-sm font-semibold capitalize'}>
{props.account.name}
</span>
</div>
<Badge variant={'outline'}>Personal Account</Badge>
<If condition={isBanned}>
<Badge variant={'destructive'}>Banned</Badge>
</If>
</div>
</div>
<div className={'flex flex-col gap-y-8'}>
<SubscriptionsTable accountId={props.account.id} />
<div className={'divider-divider-x flex flex-col gap-y-2.5'}>
<Heading level={6}>Companies</Heading>
<div>
<AdminMembershipsTable memberships={memberships} />
</div>
</div>
</div>
</PageBody>
</>
);
}
async function TeamAccountPage(props: {
account: Account & { memberships: Membership[] };
}) {
const members = await getMembers(props.account.slug ?? '');
return (
<>
<PageHeader
className="border-b"
description={
<AppBreadcrumbs
values={{
[props.account.id]:
props.account.name ?? props.account.email ?? 'Account',
}}
/>
}
>
<AdminDeleteAccountDialog accountId={props.account.id}>
<Button
size={'sm'}
variant={'destructive'}
data-test={'admin-delete-account-button'}
>
<BadgeX className={'mr-1 h-4'} />
Delete
</Button>
</AdminDeleteAccountDialog>
</PageHeader>
<PageBody className={'space-y-6 py-4'}>
<div className={'flex justify-between'}>
<div className={'flex items-center gap-x-4'}>
<div className={'flex items-center gap-x-2.5'}>
<ProfileAvatar
pictureUrl={props.account.picture_url}
displayName={props.account.name}
/>
<span className={'text-sm font-semibold capitalize'}>
{props.account.name}
</span>
</div>
<Badge variant={'outline'}>Company Account</Badge>
</div>
</div>
<div>
<div className={'flex flex-col gap-y-8'}>
<div className={'flex flex-col gap-y-2.5'}>
<Heading level={6}>Company Employees</Heading>
<AdminMembersTable members={members} />
</div>
</div>
</div>
</PageBody>
</>
);
}
async function SubscriptionsTable(props: { accountId: string }) {
const client = getSupabaseServerClient();
const { data: subscription, error } = await client
.from('subscriptions')
.select('*, subscription_items !inner (*)')
.eq('account_id', props.accountId)
.maybeSingle();
if (error) {
return (
<Alert variant={'destructive'}>
<AlertTitle>There was an error loading subscription.</AlertTitle>
<AlertDescription>
Please check the logs for more information or try again later.
</AlertDescription>
</Alert>
);
}
return (
<div className={'flex flex-col gap-y-1'}>
<Heading level={6}>Subscription</Heading>
<If
condition={subscription}
fallback={
<span className={'text-muted-foreground text-sm'}>
This account does not currently have a subscription.
</span>
}
>
{(subscription) => {
return (
<div className={'flex flex-col space-y-4'}>
<Table>
<TableHeader>
<TableHead>Subscription ID</TableHead>
<TableHead>Provider</TableHead>
<TableHead>Customer ID</TableHead>
<TableHead>Status</TableHead>
<TableHead>Created At</TableHead>
<TableHead>Period Starts At</TableHead>
<TableHead>Ends At</TableHead>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>
<span>{subscription.id}</span>
</TableCell>
<TableCell>
<span>{subscription.billing_provider}</span>
</TableCell>
<TableCell>
<span>{subscription.billing_customer_id}</span>
</TableCell>
<TableCell>
<span>{subscription.status}</span>
</TableCell>
<TableCell>
<span>{subscription.created_at}</span>
</TableCell>
<TableCell>
<span>{subscription.period_starts_at}</span>
</TableCell>
<TableCell>
<span>{subscription.period_ends_at}</span>
</TableCell>
</TableRow>
</TableBody>
</Table>
<Table>
<TableHeader>
<TableHead>Product ID</TableHead>
<TableHead>Variant ID</TableHead>
<TableHead>Quantity</TableHead>
<TableHead>Price</TableHead>
<TableHead>Interval</TableHead>
<TableHead>Type</TableHead>
</TableHeader>
<TableBody>
{subscription.subscription_items.map((item) => {
return (
<TableRow key={item.variant_id}>
<TableCell>
<span>{item.product_id}</span>
</TableCell>
<TableCell>
<span>{item.variant_id}</span>
</TableCell>
<TableCell>
<span>{item.quantity}</span>
</TableCell>
<TableCell>
<span>{item.price_amount}</span>
</TableCell>
<TableCell>
<span>{item.interval}</span>
</TableCell>
<TableCell>
<span>{item.type}</span>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
);
}}
</If>
</div>
);
}
async function getMemberships(userId: string) {
const client = getSupabaseServerClient();
const memberships = await client
.from('accounts_memberships')
.select<
string,
Membership & {
account: {
id: string;
name: string;
};
}
>('*, account: account_id !inner (id, name)')
.eq('user_id', userId);
if (memberships.error) {
throw memberships.error;
}
return memberships.data;
}
async function getMembers(accountSlug: string) {
const client = getSupabaseServerClient();
const members = await client.rpc('get_account_members', {
account_slug: accountSlug,
});
if (members.error) {
throw members.error;
}
return members.data;
}