391 lines
11 KiB
TypeScript
391 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 { createTeamAccountsApi } from '@kit/team-accounts/api';
|
|
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<{ schema: 'medreport' }, 'accounts'>;
|
|
type Membership = Tables<{ schema: 'medreport' }, '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'}>
|
|
<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 api = createTeamAccountsApi(getSupabaseServerClient());
|
|
const members = await api.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 Members</Heading>
|
|
|
|
<AdminMembersTable members={members} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</PageBody>
|
|
</>
|
|
);
|
|
}
|
|
|
|
async function SubscriptionsTable(props: { accountId: string }) {
|
|
const client = getSupabaseServerClient();
|
|
|
|
const { data: subscription, error } = await client
|
|
.schema('medreport')
|
|
.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
|
|
.schema('medreport')
|
|
.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;
|
|
}
|