mirror of
https://github.com/documenso/documenso.git
synced 2025-11-15 01:01:49 +10:00
chore: minor changes
This commit is contained in:
@ -1,70 +0,0 @@
|
|||||||
import { useMemo } from 'react';
|
|
||||||
|
|
||||||
import { Trans } from '@lingui/react/macro';
|
|
||||||
import { useLocation, useNavigate, useSearchParams } from 'react-router';
|
|
||||||
|
|
||||||
import type { PeriodSelectorValue } from '@documenso/lib/server-only/document/find-documents';
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@documenso/ui/primitives/select';
|
|
||||||
|
|
||||||
const isPeriodSelectorValue = (value: unknown): value is PeriodSelectorValue => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
||||||
return ['', '7d', '14d', '30d'].includes(value as string);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PeriodSelector = () => {
|
|
||||||
const { pathname } = useLocation();
|
|
||||||
const [searchParams] = useSearchParams();
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const period = useMemo(() => {
|
|
||||||
const p = searchParams?.get('period') ?? 'all';
|
|
||||||
|
|
||||||
return isPeriodSelectorValue(p) ? p : 'all';
|
|
||||||
}, [searchParams]);
|
|
||||||
|
|
||||||
const onPeriodChange = (newPeriod: string) => {
|
|
||||||
if (!pathname) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = new URLSearchParams(searchParams?.toString());
|
|
||||||
|
|
||||||
params.set('period', newPeriod);
|
|
||||||
|
|
||||||
if (newPeriod === '' || newPeriod === 'all') {
|
|
||||||
params.delete('period');
|
|
||||||
}
|
|
||||||
|
|
||||||
void navigate(`${pathname}?${params.toString()}`, { preventScrollReset: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Select defaultValue={period} onValueChange={onPeriodChange}>
|
|
||||||
<SelectTrigger className="text-muted-foreground max-w-[200px]">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
|
|
||||||
<SelectContent position="popper">
|
|
||||||
<SelectItem value="all">
|
|
||||||
<Trans>All Time</Trans>
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="7d">
|
|
||||||
<Trans>Last 7 days</Trans>
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="14d">
|
|
||||||
<Trans>Last 14 days</Trans>
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="30d">
|
|
||||||
<Trans>Last 30 days</Trans>
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,17 +1,17 @@
|
|||||||
import type { Prisma, User } from '@prisma/client';
|
import type { Prisma, User } from '@prisma/client';
|
||||||
import { DocumentVisibility, SigningStatus, TeamMemberRole } from '@prisma/client';
|
import { DocumentVisibility, SigningStatus, TeamMemberRole } from '@prisma/client';
|
||||||
import { DateTime } from 'luxon';
|
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import type { PeriodSelectorValue } from '@documenso/lib/server-only/document/find-documents';
|
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { isExtendedDocumentStatus } from '@documenso/prisma/guards/is-extended-document-status';
|
import { isExtendedDocumentStatus } from '@documenso/prisma/guards/is-extended-document-status';
|
||||||
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
||||||
|
import type { TimePeriod } from '@documenso/ui/primitives/data-table/utils/time-filters';
|
||||||
|
import { getDateRangeForPeriod } from '@documenso/ui/primitives/data-table/utils/time-filters';
|
||||||
|
|
||||||
export type GetStatsInput = {
|
export type GetStatsInput = {
|
||||||
user: User;
|
user: User;
|
||||||
team?: Omit<GetTeamCountsOption, 'createdAt'>;
|
team?: Omit<GetTeamCountsOption, 'createdAt'>;
|
||||||
period?: PeriodSelectorValue;
|
period?: TimePeriod;
|
||||||
search?: string;
|
search?: string;
|
||||||
folderId?: string;
|
folderId?: string;
|
||||||
};
|
};
|
||||||
@ -26,60 +26,14 @@ export const getStats = async ({
|
|||||||
let createdAt: Prisma.DocumentWhereInput['createdAt'];
|
let createdAt: Prisma.DocumentWhereInput['createdAt'];
|
||||||
|
|
||||||
if (period && period !== 'all-time') {
|
if (period && period !== 'all-time') {
|
||||||
const now = DateTime.now();
|
const dateRange = getDateRangeForPeriod(period);
|
||||||
let startDate: DateTime;
|
|
||||||
let endDate: DateTime;
|
|
||||||
|
|
||||||
switch (period) {
|
if (dateRange) {
|
||||||
case 'today':
|
createdAt = {
|
||||||
startDate = now.startOf('day');
|
gte: dateRange.start.toJSDate(),
|
||||||
endDate = now.endOf('day');
|
lte: dateRange.end.toJSDate(),
|
||||||
break;
|
};
|
||||||
case 'yesterday':
|
|
||||||
startDate = now.minus({ days: 1 }).startOf('day');
|
|
||||||
endDate = now.minus({ days: 1 }).endOf('day');
|
|
||||||
break;
|
|
||||||
case 'this-week':
|
|
||||||
startDate = now.startOf('week');
|
|
||||||
endDate = now.endOf('week');
|
|
||||||
break;
|
|
||||||
case 'last-week':
|
|
||||||
startDate = now.minus({ weeks: 1 }).startOf('week');
|
|
||||||
endDate = now.minus({ weeks: 1 }).endOf('week');
|
|
||||||
break;
|
|
||||||
case 'this-month':
|
|
||||||
startDate = now.startOf('month');
|
|
||||||
endDate = now.endOf('month');
|
|
||||||
break;
|
|
||||||
case 'last-month':
|
|
||||||
startDate = now.minus({ months: 1 }).startOf('month');
|
|
||||||
endDate = now.minus({ months: 1 }).endOf('month');
|
|
||||||
break;
|
|
||||||
case 'this-quarter':
|
|
||||||
startDate = now.startOf('quarter');
|
|
||||||
endDate = now.endOf('quarter');
|
|
||||||
break;
|
|
||||||
case 'last-quarter':
|
|
||||||
startDate = now.minus({ quarters: 1 }).startOf('quarter');
|
|
||||||
endDate = now.minus({ quarters: 1 }).endOf('quarter');
|
|
||||||
break;
|
|
||||||
case 'this-year':
|
|
||||||
startDate = now.startOf('year');
|
|
||||||
endDate = now.endOf('year');
|
|
||||||
break;
|
|
||||||
case 'last-year':
|
|
||||||
startDate = now.minus({ years: 1 }).startOf('year');
|
|
||||||
endDate = now.minus({ years: 1 }).endOf('year');
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
startDate = now.startOf('day');
|
|
||||||
endDate = now.endOf('day');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createdAt = {
|
|
||||||
gte: startDate.toJSDate(),
|
|
||||||
lte: endDate.toJSDate(),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const [ownerCounts, notSignedCounts, hasSignedCounts] = await (options.team
|
const [ownerCounts, notSignedCounts, hasSignedCounts] = await (options.team
|
||||||
|
|||||||
167
packages/ui/primitives/data-table.tsx
Normal file
167
packages/ui/primitives/data-table.tsx
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import type {
|
||||||
|
ColumnDef,
|
||||||
|
PaginationState,
|
||||||
|
Table as TTable,
|
||||||
|
Updater,
|
||||||
|
VisibilityState,
|
||||||
|
} from '@tanstack/react-table';
|
||||||
|
import { flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
import { Skeleton } from './skeleton';
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './table';
|
||||||
|
|
||||||
|
export type DataTableChildren<TData> = (_table: TTable<TData>) => React.ReactNode;
|
||||||
|
|
||||||
|
export type { ColumnDef as DataTableColumnDef } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
export interface DataTableProps<TData, TValue> {
|
||||||
|
columns: ColumnDef<TData, TValue>[];
|
||||||
|
columnVisibility?: VisibilityState;
|
||||||
|
data: TData[];
|
||||||
|
perPage?: number;
|
||||||
|
currentPage?: number;
|
||||||
|
totalPages?: number;
|
||||||
|
onPaginationChange?: (_page: number, _perPage: number) => void;
|
||||||
|
onClearFilters?: () => void;
|
||||||
|
hasFilters?: boolean;
|
||||||
|
children?: DataTableChildren<TData>;
|
||||||
|
skeleton?: {
|
||||||
|
enable: boolean;
|
||||||
|
rows: number;
|
||||||
|
component?: React.ReactNode;
|
||||||
|
};
|
||||||
|
error?: {
|
||||||
|
enable: boolean;
|
||||||
|
component?: React.ReactNode;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DataTable<TData, TValue>({
|
||||||
|
columns,
|
||||||
|
columnVisibility,
|
||||||
|
data,
|
||||||
|
error,
|
||||||
|
perPage,
|
||||||
|
currentPage,
|
||||||
|
totalPages,
|
||||||
|
skeleton,
|
||||||
|
hasFilters,
|
||||||
|
onClearFilters,
|
||||||
|
onPaginationChange,
|
||||||
|
children,
|
||||||
|
}: DataTableProps<TData, TValue>) {
|
||||||
|
const pagination = useMemo<PaginationState>(() => {
|
||||||
|
if (currentPage !== undefined && perPage !== undefined) {
|
||||||
|
return {
|
||||||
|
pageIndex: currentPage - 1,
|
||||||
|
pageSize: perPage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 0,
|
||||||
|
};
|
||||||
|
}, [currentPage, perPage]);
|
||||||
|
|
||||||
|
const manualPagination = Boolean(currentPage !== undefined && totalPages !== undefined);
|
||||||
|
|
||||||
|
const onTablePaginationChange = (updater: Updater<PaginationState>) => {
|
||||||
|
if (typeof updater === 'function') {
|
||||||
|
const newState = updater(pagination);
|
||||||
|
|
||||||
|
onPaginationChange?.(newState.pageIndex + 1, newState.pageSize);
|
||||||
|
} else {
|
||||||
|
onPaginationChange?.(updater.pageIndex + 1, updater.pageSize);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data,
|
||||||
|
columns,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
state: {
|
||||||
|
pagination: manualPagination ? pagination : undefined,
|
||||||
|
columnVisibility,
|
||||||
|
},
|
||||||
|
manualPagination,
|
||||||
|
pageCount: totalPages,
|
||||||
|
onPaginationChange: onTablePaginationChange,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="rounded-md border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<TableRow key={headerGroup.id}>
|
||||||
|
{headerGroup.headers.map((header) => {
|
||||||
|
return (
|
||||||
|
<TableHead key={header.id}>
|
||||||
|
{header.isPlaceholder
|
||||||
|
? null
|
||||||
|
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||||
|
</TableHead>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{table.getRowModel().rows?.length ? (
|
||||||
|
table.getRowModel().rows.map((row) => (
|
||||||
|
<TableRow key={row.id} data-state={row.getIsSelected() && 'selected'}>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell
|
||||||
|
key={cell.id}
|
||||||
|
style={{
|
||||||
|
width: `${cell.column.getSize()}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : error?.enable ? (
|
||||||
|
<TableRow>
|
||||||
|
{error.component ?? (
|
||||||
|
<TableCell colSpan={columns.length} className="h-32 text-center">
|
||||||
|
<Trans>Something went wrong.</Trans>
|
||||||
|
</TableCell>
|
||||||
|
)}
|
||||||
|
</TableRow>
|
||||||
|
) : skeleton?.enable ? (
|
||||||
|
Array.from({ length: skeleton.rows }).map((_, i) => (
|
||||||
|
<TableRow key={`skeleton-row-${i}`}>{skeleton.component ?? <Skeleton />}</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={columns.length} className="h-32 text-center">
|
||||||
|
<p>
|
||||||
|
<Trans>No results found</Trans>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{hasFilters && onClearFilters !== undefined && (
|
||||||
|
<button
|
||||||
|
onClick={() => onClearFilters()}
|
||||||
|
className="text-foreground mt-1 text-sm"
|
||||||
|
>
|
||||||
|
<Trans>Clear filters</Trans>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{children && <div className="mt-8 w-full">{children(table)}</div>}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user