mirror of
https://github.com/documenso/documenso.git
synced 2025-11-20 11:41:44 +10:00
feat: org insights (#1937)
This commit is contained in:
@ -114,13 +114,13 @@ export default function AdminLayout() {
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
'justify-start md:w-full',
|
||||
pathname?.startsWith('/admin/leaderboard') && 'bg-secondary',
|
||||
pathname?.startsWith('/admin/organisation-insights') && 'bg-secondary',
|
||||
)}
|
||||
asChild
|
||||
>
|
||||
<Link to="/admin/leaderboard">
|
||||
<Link to="/admin/organisation-insights">
|
||||
<Trophy className="mr-2 h-5 w-5" />
|
||||
<Trans>Leaderboard</Trans>
|
||||
<Trans>Organisation Insights</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
@ -128,7 +128,7 @@ export default function AdminLayout() {
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
'justify-start md:w-full',
|
||||
pathname?.startsWith('/admin/banner') && 'bg-secondary',
|
||||
pathname?.startsWith('/admin/site-settings') && 'bg-secondary',
|
||||
)}
|
||||
asChild
|
||||
>
|
||||
|
||||
@ -1,77 +0,0 @@
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
|
||||
import { getSigningVolume } from '@documenso/lib/server-only/admin/get-signing-volume';
|
||||
|
||||
import {
|
||||
AdminLeaderboardTable,
|
||||
type SigningVolume,
|
||||
} from '~/components/tables/admin-leaderboard-table';
|
||||
|
||||
import type { Route } from './+types/leaderboard';
|
||||
|
||||
export async function loader({ request }: Route.LoaderArgs) {
|
||||
const url = new URL(request.url);
|
||||
|
||||
const rawSortBy = url.searchParams.get('sortBy') || 'signingVolume';
|
||||
const rawSortOrder = url.searchParams.get('sortOrder') || 'desc';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
const sortOrder = (['asc', 'desc'].includes(rawSortOrder) ? rawSortOrder : 'desc') as
|
||||
| 'asc'
|
||||
| 'desc';
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
const sortBy = (
|
||||
['name', 'createdAt', 'signingVolume'].includes(rawSortBy) ? rawSortBy : 'signingVolume'
|
||||
) as 'name' | 'createdAt' | 'signingVolume';
|
||||
|
||||
const page = Number(url.searchParams.get('page')) || 1;
|
||||
const perPage = Number(url.searchParams.get('perPage')) || 10;
|
||||
const search = url.searchParams.get('search') || '';
|
||||
|
||||
const { leaderboard, totalPages } = await getSigningVolume({
|
||||
search,
|
||||
page,
|
||||
perPage,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
});
|
||||
|
||||
const typedSigningVolume: SigningVolume[] = leaderboard.map((item) => ({
|
||||
...item,
|
||||
name: item.name || '',
|
||||
createdAt: item.createdAt || new Date(),
|
||||
}));
|
||||
|
||||
return {
|
||||
signingVolume: typedSigningVolume,
|
||||
totalPages,
|
||||
page,
|
||||
perPage,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
};
|
||||
}
|
||||
|
||||
export default function Leaderboard({ loaderData }: Route.ComponentProps) {
|
||||
const { signingVolume, totalPages, page, perPage, sortBy, sortOrder } = loaderData;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center">
|
||||
<h2 className="text-4xl font-semibold">
|
||||
<Trans>Signing Volume</Trans>
|
||||
</h2>
|
||||
</div>
|
||||
<div className="mt-8">
|
||||
<AdminLeaderboardTable
|
||||
signingVolume={signingVolume}
|
||||
totalPages={totalPages}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
sortBy={sortBy}
|
||||
sortOrder={sortOrder}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,59 @@
|
||||
import { getOrganisationDetailedInsights } from '@documenso/lib/server-only/admin/get-organisation-detailed-insights';
|
||||
import type { DateRange } from '@documenso/lib/types/search-params';
|
||||
import { getAdminOrganisation } from '@documenso/trpc/server/admin-router/get-admin-organisation';
|
||||
|
||||
import { OrganisationInsightsTable } from '~/components/tables/organisation-insights-table';
|
||||
|
||||
import type { Route } from './+types/organisation-insights.$id';
|
||||
|
||||
export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
const { id } = params;
|
||||
const url = new URL(request.url);
|
||||
|
||||
const page = Number(url.searchParams.get('page')) || 1;
|
||||
const perPage = Number(url.searchParams.get('perPage')) || 10;
|
||||
const dateRange = (url.searchParams.get('dateRange') || 'last30days') as DateRange;
|
||||
const view = (url.searchParams.get('view') || 'teams') as 'teams' | 'users' | 'documents';
|
||||
|
||||
const [insights, organisation] = await Promise.all([
|
||||
getOrganisationDetailedInsights({
|
||||
organisationId: id,
|
||||
page,
|
||||
perPage,
|
||||
dateRange,
|
||||
view,
|
||||
}),
|
||||
getAdminOrganisation({ organisationId: id }),
|
||||
]);
|
||||
|
||||
return {
|
||||
organisationId: id,
|
||||
organisationName: organisation.name,
|
||||
insights,
|
||||
page,
|
||||
perPage,
|
||||
dateRange,
|
||||
view,
|
||||
};
|
||||
}
|
||||
|
||||
export default function OrganisationInsights({ loaderData }: Route.ComponentProps) {
|
||||
const { insights, page, perPage, dateRange, view, organisationName } = loaderData;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-4xl font-semibold">{organisationName}</h2>
|
||||
</div>
|
||||
<div className="mt-8">
|
||||
<OrganisationInsightsTable
|
||||
insights={insights}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
dateRange={dateRange}
|
||||
view={view}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,91 @@
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
|
||||
import { getOrganisationInsights } from '@documenso/lib/server-only/admin/get-signing-volume';
|
||||
import type { DateRange } from '@documenso/lib/types/search-params';
|
||||
|
||||
import { DateRangeFilter } from '~/components/filters/date-range-filter';
|
||||
import {
|
||||
AdminOrganisationOverviewTable,
|
||||
type OrganisationOverview,
|
||||
} from '~/components/tables/admin-organisation-overview-table';
|
||||
|
||||
import type { Route } from './+types/organisation-insights._index';
|
||||
|
||||
export async function loader({ request }: Route.LoaderArgs) {
|
||||
const url = new URL(request.url);
|
||||
|
||||
const rawSortBy = url.searchParams.get('sortBy') || 'signingVolume';
|
||||
const rawSortOrder = url.searchParams.get('sortOrder') || 'desc';
|
||||
|
||||
const isSortOrder = (value: string): value is 'asc' | 'desc' =>
|
||||
value === 'asc' || value === 'desc';
|
||||
const isSortBy = (value: string): value is 'name' | 'createdAt' | 'signingVolume' =>
|
||||
value === 'name' || value === 'createdAt' || value === 'signingVolume';
|
||||
|
||||
const sortOrder: 'asc' | 'desc' = isSortOrder(rawSortOrder) ? rawSortOrder : 'desc';
|
||||
const sortBy: 'name' | 'createdAt' | 'signingVolume' = isSortBy(rawSortBy)
|
||||
? rawSortBy
|
||||
: 'signingVolume';
|
||||
|
||||
const page = Number(url.searchParams.get('page')) || 1;
|
||||
const perPage = Number(url.searchParams.get('perPage')) || 10;
|
||||
const search = url.searchParams.get('search') || '';
|
||||
const dateRange = (url.searchParams.get('dateRange') || 'last30days') as DateRange;
|
||||
|
||||
const { organisations, totalPages } = await getOrganisationInsights({
|
||||
search,
|
||||
page,
|
||||
perPage,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
dateRange,
|
||||
});
|
||||
|
||||
const typedOrganisations: OrganisationOverview[] = organisations.map((item) => ({
|
||||
id: String(item.id),
|
||||
name: item.name || '',
|
||||
signingVolume: item.signingVolume,
|
||||
createdAt: item.createdAt || new Date(),
|
||||
customerId: item.customerId || '',
|
||||
subscriptionStatus: item.subscriptionStatus,
|
||||
teamCount: item.teamCount || 0,
|
||||
memberCount: item.memberCount || 0,
|
||||
}));
|
||||
|
||||
return {
|
||||
organisations: typedOrganisations,
|
||||
totalPages,
|
||||
page,
|
||||
perPage,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
dateRange,
|
||||
};
|
||||
}
|
||||
|
||||
export default function Organisations({ loaderData }: Route.ComponentProps) {
|
||||
const { organisations, totalPages, page, perPage, sortBy, sortOrder, dateRange } = loaderData;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-4xl font-semibold">
|
||||
<Trans>Organisation Insights</Trans>
|
||||
</h2>
|
||||
<DateRangeFilter currentRange={dateRange} />
|
||||
</div>
|
||||
|
||||
<div className="mt-8">
|
||||
<AdminOrganisationOverviewTable
|
||||
organisations={organisations}
|
||||
totalPages={totalPages}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
sortBy={sortBy}
|
||||
sortOrder={sortOrder}
|
||||
dateRange={dateRange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user