mirror of
https://github.com/documenso/documenso.git
synced 2025-11-20 19:51:32 +10:00
chore: minor changes
This commit is contained in:
@ -4,6 +4,7 @@ import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
|
||||
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
|
||||
import type { DateRange } from '@documenso/lib/types/search-params';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@ -13,7 +14,7 @@ import {
|
||||
} from '@documenso/ui/primitives/select';
|
||||
|
||||
type DateRangeFilterProps = {
|
||||
currentRange: string;
|
||||
currentRange: DateRange;
|
||||
};
|
||||
|
||||
export const DateRangeFilter = ({ currentRange }: DateRangeFilterProps) => {
|
||||
@ -24,7 +25,7 @@ export const DateRangeFilter = ({ currentRange }: DateRangeFilterProps) => {
|
||||
const handleRangeChange = (value: string) => {
|
||||
startTransition(() => {
|
||||
updateSearchParams({
|
||||
dateRange: value,
|
||||
dateRange: value as DateRange,
|
||||
page: 1,
|
||||
});
|
||||
});
|
||||
@ -32,7 +33,6 @@ export const DateRangeFilter = ({ currentRange }: DateRangeFilterProps) => {
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium">{_(msg`Time Range:`)}</span>
|
||||
<Select value={currentRange} onValueChange={handleRangeChange} disabled={isPending}>
|
||||
<SelectTrigger className="w-48">
|
||||
<SelectValue />
|
||||
|
||||
@ -3,6 +3,7 @@ import { useEffect, useMemo, useState, useTransition } from 'react';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { ChevronDownIcon, ChevronUpIcon, ChevronsUpDown, Loader } from 'lucide-react';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value';
|
||||
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
|
||||
@ -16,7 +17,7 @@ export type OrganisationOverview = {
|
||||
name: string;
|
||||
signingVolume: number;
|
||||
createdAt: Date;
|
||||
planId: string;
|
||||
customerId: string;
|
||||
subscriptionStatus?: string;
|
||||
isActive?: boolean;
|
||||
teamCount?: number;
|
||||
@ -71,16 +72,16 @@ export const AdminOrganisationOverviewTable = ({
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div>
|
||||
<a
|
||||
<Link
|
||||
className="text-primary underline"
|
||||
href={`/admin/organisation-insights/${row.original.id}`}
|
||||
to={`/admin/organisation-insights/${row.original.id}`}
|
||||
>
|
||||
{row.getValue('name')}
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
size: 200,
|
||||
size: 240,
|
||||
},
|
||||
{
|
||||
header: () => (
|
||||
@ -88,7 +89,7 @@ export const AdminOrganisationOverviewTable = ({
|
||||
className="flex cursor-pointer items-center"
|
||||
onClick={() => handleColumnSort('signingVolume')}
|
||||
>
|
||||
{_(msg`Document Volume`)}
|
||||
<span className="whitespace-nowrap">{_(msg`Document Volume`)}</span>
|
||||
{sortBy === 'signingVolume' ? (
|
||||
sortOrder === 'asc' ? (
|
||||
<ChevronUpIcon className="ml-2 h-4 w-4" />
|
||||
@ -102,26 +103,7 @@ export const AdminOrganisationOverviewTable = ({
|
||||
),
|
||||
accessorKey: 'signingVolume',
|
||||
cell: ({ row }) => <div>{Number(row.getValue('signingVolume'))}</div>,
|
||||
size: 120,
|
||||
},
|
||||
{
|
||||
header: () => {
|
||||
return <div>{_(msg`Status`)}</div>;
|
||||
},
|
||||
accessorKey: 'subscriptionStatus',
|
||||
cell: ({ row }) => {
|
||||
const status = row.original.subscriptionStatus;
|
||||
return (
|
||||
<div
|
||||
className={`inline-flex items-center rounded-full px-2 py-1 text-xs font-medium ${
|
||||
status ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
|
||||
}`}
|
||||
>
|
||||
{status || 'Free'}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
size: 100,
|
||||
size: 160,
|
||||
},
|
||||
{
|
||||
header: () => {
|
||||
@ -129,7 +111,7 @@ export const AdminOrganisationOverviewTable = ({
|
||||
},
|
||||
accessorKey: 'teamCount',
|
||||
cell: ({ row }) => <div>{Number(row.original.teamCount) || 0}</div>,
|
||||
size: 80,
|
||||
size: 120,
|
||||
},
|
||||
{
|
||||
header: () => {
|
||||
@ -137,7 +119,7 @@ export const AdminOrganisationOverviewTable = ({
|
||||
},
|
||||
accessorKey: 'memberCount',
|
||||
cell: ({ row }) => <div>{Number(row.original.memberCount) || 0}</div>,
|
||||
size: 80,
|
||||
size: 160,
|
||||
},
|
||||
{
|
||||
header: () => {
|
||||
@ -146,7 +128,7 @@ export const AdminOrganisationOverviewTable = ({
|
||||
className="flex cursor-pointer items-center"
|
||||
onClick={() => handleColumnSort('createdAt')}
|
||||
>
|
||||
{_(msg`Created`)}
|
||||
<span className="whitespace-nowrap">{_(msg`Created`)}</span>
|
||||
{sortBy === 'createdAt' ? (
|
||||
sortOrder === 'asc' ? (
|
||||
<ChevronUpIcon className="ml-2 h-4 w-4" />
|
||||
@ -160,7 +142,7 @@ export const AdminOrganisationOverviewTable = ({
|
||||
);
|
||||
},
|
||||
accessorKey: 'createdAt',
|
||||
cell: ({ row }) => i18n.date(row.original.createdAt),
|
||||
cell: ({ row }) => i18n.date(new Date(row.original.createdAt)),
|
||||
size: 120,
|
||||
},
|
||||
] satisfies DataTableColumnDef<OrganisationOverview>[];
|
||||
|
||||
@ -93,7 +93,8 @@ export const AdminOrganisationsTable = ({
|
||||
),
|
||||
},
|
||||
{
|
||||
header: t`Status`,
|
||||
id: 'role',
|
||||
header: t`Role`,
|
||||
cell: ({ row }) => (
|
||||
<Badge variant="neutral">
|
||||
{row.original.owner.id === memberUserId ? t`Owner` : t`Member`}
|
||||
@ -101,6 +102,7 @@ export const AdminOrganisationsTable = ({
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'billingStatus',
|
||||
header: t`Status`,
|
||||
cell: ({ row }) => {
|
||||
const subscription = row.original.subscription;
|
||||
@ -111,7 +113,7 @@ export const AdminOrganisationsTable = ({
|
||||
isPaid ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
|
||||
}`}
|
||||
>
|
||||
{isPaid ? 'Paid' : 'Free'}
|
||||
{isPaid ? t`Paid` : t`Free`}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
@ -184,7 +186,7 @@ export const AdminOrganisationsTable = ({
|
||||
onPaginationChange={onPaginationChange}
|
||||
columnVisibility={{
|
||||
owner: showOwnerColumn,
|
||||
status: memberUserId !== undefined,
|
||||
role: memberUserId !== undefined,
|
||||
}}
|
||||
error={{
|
||||
enable: isLoadingError,
|
||||
|
||||
@ -6,18 +6,21 @@ import { Archive, Building2, Loader, TrendingUp, Users } from 'lucide-react';
|
||||
|
||||
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
|
||||
import type { OrganisationDetailedInsights } from '@documenso/lib/server-only/admin/get-organisation-detailed-insights';
|
||||
import type { DateRange } from '@documenso/lib/types/search-params';
|
||||
import type { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import type { DataTableColumnDef } from '@documenso/ui/primitives/data-table';
|
||||
import { DataTable } from '@documenso/ui/primitives/data-table';
|
||||
import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination';
|
||||
|
||||
import { DateRangeFilter } from '~/components/filters/date-range-filter';
|
||||
import { DocumentStatus } from '~/components/general/document/document-status';
|
||||
|
||||
type OrganisationInsightsTableProps = {
|
||||
insights: OrganisationDetailedInsights;
|
||||
page: number;
|
||||
perPage: number;
|
||||
dateRange: string;
|
||||
dateRange: DateRange;
|
||||
view: 'teams' | 'users' | 'documents';
|
||||
};
|
||||
|
||||
@ -54,82 +57,104 @@ export const OrganisationInsightsTable = ({
|
||||
{
|
||||
header: _(msg`Team Name`),
|
||||
accessorKey: 'name',
|
||||
cell: ({ row }) => row.getValue('name'),
|
||||
cell: ({ row }) => <span className="block max-w-full truncate">{row.getValue('name')}</span>,
|
||||
size: 240,
|
||||
},
|
||||
{
|
||||
header: _(msg`Members`),
|
||||
accessorKey: 'memberCount',
|
||||
cell: ({ row }) => Number(row.getValue('memberCount')),
|
||||
size: 120,
|
||||
},
|
||||
{
|
||||
header: _(msg`Documents`),
|
||||
accessorKey: 'documentCount',
|
||||
cell: ({ row }) => Number(row.getValue('documentCount')),
|
||||
size: 140,
|
||||
},
|
||||
{
|
||||
header: _(msg`Created`),
|
||||
accessorKey: 'createdAt',
|
||||
cell: ({ row }) => i18n.date(row.getValue('createdAt')),
|
||||
cell: ({ row }) => i18n.date(new Date(row.getValue('createdAt'))),
|
||||
size: 160,
|
||||
},
|
||||
] satisfies DataTableColumnDef<(typeof insights.teams)[number]>[];
|
||||
|
||||
const usersColumns = [
|
||||
{
|
||||
header: _(msg`Name`),
|
||||
header: () => <span className="whitespace-nowrap">{_(msg`Name`)}</span>,
|
||||
accessorKey: 'name',
|
||||
cell: ({ row }) => row.getValue('name') || row.getValue('email'),
|
||||
cell: ({ row }) => (
|
||||
<span className="block max-w-full truncate">
|
||||
{(row.getValue('name') as string) || (row.getValue('email') as string)}
|
||||
</span>
|
||||
),
|
||||
size: 220,
|
||||
},
|
||||
{
|
||||
header: _(msg`Email`),
|
||||
header: () => <span className="whitespace-nowrap">{_(msg`Email`)}</span>,
|
||||
accessorKey: 'email',
|
||||
cell: ({ row }) => row.getValue('email'),
|
||||
cell: ({ row }) => <span className="block max-w-full truncate">{row.getValue('email')}</span>,
|
||||
size: 260,
|
||||
},
|
||||
{
|
||||
header: _(msg`Documents Created`),
|
||||
header: () => <span className="whitespace-nowrap">{_(msg`Documents Created`)}</span>,
|
||||
accessorKey: 'documentCount',
|
||||
cell: ({ row }) => Number(row.getValue('documentCount')),
|
||||
size: 180,
|
||||
},
|
||||
{
|
||||
header: _(msg`Documents Signed`),
|
||||
header: () => <span className="whitespace-nowrap">{_(msg`Documents Signed`)}</span>,
|
||||
accessorKey: 'signedDocumentCount',
|
||||
cell: ({ row }) => Number(row.getValue('signedDocumentCount')),
|
||||
size: 180,
|
||||
},
|
||||
{
|
||||
header: _(msg`Joined`),
|
||||
header: () => <span className="whitespace-nowrap">{_(msg`Joined`)}</span>,
|
||||
accessorKey: 'createdAt',
|
||||
cell: ({ row }) => i18n.date(row.getValue('createdAt')),
|
||||
cell: ({ row }) => i18n.date(new Date(row.getValue('createdAt'))),
|
||||
size: 160,
|
||||
},
|
||||
] satisfies DataTableColumnDef<(typeof insights.users)[number]>[];
|
||||
|
||||
const documentsColumns = [
|
||||
{
|
||||
header: _(msg`Title`),
|
||||
header: () => <span className="whitespace-nowrap">{_(msg`Title`)}</span>,
|
||||
accessorKey: 'title',
|
||||
cell: ({ row }) => row.getValue('title'),
|
||||
cell: ({ row }) => <span className="block max-w-full truncate">{row.getValue('title')}</span>,
|
||||
size: 240,
|
||||
},
|
||||
{
|
||||
header: _(msg`Status`),
|
||||
header: () => <span className="whitespace-nowrap">{_(msg`Status`)}</span>,
|
||||
accessorKey: 'status',
|
||||
cell: ({ row }) => row.getValue('status'),
|
||||
cell: ({ row }) => (
|
||||
<DocumentStatus status={row.getValue('status') as ExtendedDocumentStatus} />
|
||||
),
|
||||
size: 140,
|
||||
},
|
||||
{
|
||||
header: _(msg`Team`),
|
||||
header: () => <span className="whitespace-nowrap">{_(msg`Team`)}</span>,
|
||||
accessorKey: 'teamName',
|
||||
cell: ({ row }) => row.getValue('teamName'),
|
||||
cell: ({ row }) => (
|
||||
<span className="block max-w-full truncate">{row.getValue('teamName')}</span>
|
||||
),
|
||||
size: 180,
|
||||
},
|
||||
{
|
||||
header: _(msg`Created`),
|
||||
header: () => <span className="whitespace-nowrap">{_(msg`Created`)}</span>,
|
||||
accessorKey: 'createdAt',
|
||||
cell: ({ row }) => i18n.date(row.getValue('createdAt')),
|
||||
cell: ({ row }) => i18n.date(new Date(row.getValue('createdAt'))),
|
||||
size: 160,
|
||||
},
|
||||
{
|
||||
header: _(msg`Completed`),
|
||||
header: () => <span className="whitespace-nowrap">{_(msg`Completed`)}</span>,
|
||||
accessorKey: 'completedAt',
|
||||
cell: ({ row }) => {
|
||||
const completedAt = row.getValue('completedAt') as Date | null;
|
||||
|
||||
return completedAt ? i18n.date(completedAt) : '-';
|
||||
return completedAt ? i18n.date(new Date(completedAt)) : '-';
|
||||
},
|
||||
size: 160,
|
||||
},
|
||||
] satisfies DataTableColumnDef<(typeof insights.documents)[number]>[];
|
||||
|
||||
|
||||
@ -114,7 +114,7 @@ export default function AdminLayout() {
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
'justify-start md:w-full',
|
||||
pathname?.startsWith('/admin/org-insights') && 'bg-secondary',
|
||||
pathname?.startsWith('/admin/organisation-insights') && 'bg-secondary',
|
||||
)}
|
||||
asChild
|
||||
>
|
||||
@ -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
|
||||
>
|
||||
|
||||
@ -57,6 +57,7 @@ export default function AdminDocumentsPage() {
|
||||
header: _(msg`Created`),
|
||||
accessorKey: 'createdAt',
|
||||
cell: ({ row }) => i18n.date(row.original.createdAt),
|
||||
size: 160,
|
||||
},
|
||||
{
|
||||
header: _(msg`Title`),
|
||||
@ -65,17 +66,19 @@ export default function AdminDocumentsPage() {
|
||||
return (
|
||||
<Link
|
||||
to={`/admin/documents/${row.original.id}`}
|
||||
className="block max-w-[5rem] truncate font-medium hover:underline md:max-w-[10rem]"
|
||||
className="block truncate font-medium hover:underline"
|
||||
>
|
||||
{row.original.title}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
size: 240,
|
||||
},
|
||||
{
|
||||
header: _(msg`Status`),
|
||||
accessorKey: 'status',
|
||||
cell: ({ row }) => <DocumentStatus status={row.original.status} />,
|
||||
size: 140,
|
||||
},
|
||||
{
|
||||
header: _(msg`Owner`),
|
||||
@ -112,11 +115,13 @@ export default function AdminDocumentsPage() {
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
size: 100,
|
||||
},
|
||||
{
|
||||
header: 'Last updated',
|
||||
accessorKey: 'updatedAt',
|
||||
cell: ({ row }) => i18n.date(row.original.updatedAt),
|
||||
size: 160,
|
||||
},
|
||||
] satisfies DataTableColumnDef<(typeof results)['data'][number]>[];
|
||||
}, []);
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
|
||||
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';
|
||||
@ -13,11 +12,7 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
|
||||
const page = Number(url.searchParams.get('page')) || 1;
|
||||
const perPage = Number(url.searchParams.get('perPage')) || 10;
|
||||
const dateRange = (url.searchParams.get('dateRange') || 'last30days') as
|
||||
| 'last30days'
|
||||
| 'last90days'
|
||||
| 'lastYear'
|
||||
| 'allTime';
|
||||
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([
|
||||
@ -48,9 +43,7 @@ export default function OrganisationInsights({ loaderData }: Route.ComponentProp
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-4xl font-semibold">
|
||||
<Trans>{organisationName}</Trans>
|
||||
</h2>
|
||||
<h2 className="text-4xl font-semibold">{organisationName}</h2>
|
||||
</div>
|
||||
<div className="mt-8">
|
||||
<OrganisationInsightsTable
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
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 {
|
||||
@ -16,23 +17,20 @@ export async function loader({ request }: Route.LoaderArgs) {
|
||||
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 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
|
||||
| 'last30days'
|
||||
| 'last90days'
|
||||
| 'lastYear'
|
||||
| 'allTime';
|
||||
const dateRange = (url.searchParams.get('dateRange') || 'last30days') as DateRange;
|
||||
|
||||
const { organisations, totalPages } = await getOrganisationInsights({
|
||||
search,
|
||||
@ -48,7 +46,7 @@ export async function loader({ request }: Route.LoaderArgs) {
|
||||
name: item.name || '',
|
||||
signingVolume: item.signingVolume,
|
||||
createdAt: item.createdAt || new Date(),
|
||||
planId: item.customerId || '',
|
||||
customerId: item.customerId || '',
|
||||
subscriptionStatus: item.subscriptionStatus,
|
||||
teamCount: item.teamCount || 0,
|
||||
memberCount: item.memberCount || 0,
|
||||
|
||||
Reference in New Issue
Block a user