update
This commit is contained in:
@@ -1,270 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import {
|
|
||||||
flexRender,
|
|
||||||
getCoreRowModel,
|
|
||||||
useReactTable,
|
|
||||||
type ColumnDef,
|
|
||||||
type Table as TTable,
|
|
||||||
type VisibilityState
|
|
||||||
} from '@tanstack/react-table'
|
|
||||||
import {
|
|
||||||
ChevronDownIcon,
|
|
||||||
ChevronLeftIcon,
|
|
||||||
ChevronRightIcon,
|
|
||||||
Columns2Icon
|
|
||||||
} from 'lucide-react'
|
|
||||||
import { createContext, useContext, useState, type ReactNode } from 'react'
|
|
||||||
import { useSearchParams } from 'react-router'
|
|
||||||
|
|
||||||
import { Button } from '@repo/ui/components/ui/button'
|
|
||||||
import { Card, CardContent } from '@repo/ui/components/ui/card'
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuCheckboxItem,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuTrigger
|
|
||||||
} from '@repo/ui/components/ui/dropdown-menu'
|
|
||||||
import { Label } from '@repo/ui/components/ui/label'
|
|
||||||
import {
|
|
||||||
Pagination,
|
|
||||||
PaginationContent,
|
|
||||||
PaginationItem
|
|
||||||
} from '@repo/ui/components/ui/pagination'
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue
|
|
||||||
} from '@repo/ui/components/ui/select'
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow
|
|
||||||
} from '@repo/ui/components/ui/table'
|
|
||||||
import { cn } from '@repo/ui/lib/utils'
|
|
||||||
|
|
||||||
interface DataTableProps<TData, TValue> {
|
|
||||||
children?: ReactNode
|
|
||||||
columns: ColumnDef<TData, TValue>[]
|
|
||||||
data: TData[]
|
|
||||||
pageIndex: number
|
|
||||||
pageSize: number
|
|
||||||
rowCount: number
|
|
||||||
hiddenColumn?: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const TableContext = createContext<{ table: TTable<any> } | null>(null)
|
|
||||||
|
|
||||||
export function DataTable<TData, TValue>({
|
|
||||||
children,
|
|
||||||
columns,
|
|
||||||
data,
|
|
||||||
pageIndex,
|
|
||||||
pageSize,
|
|
||||||
rowCount,
|
|
||||||
hiddenColumn = []
|
|
||||||
}: DataTableProps<TData, TValue>) {
|
|
||||||
const [, setSearchParams] = useSearchParams()
|
|
||||||
const hiddenColumn_ = Object.fromEntries(
|
|
||||||
hiddenColumn.map((column) => [column, false])
|
|
||||||
)
|
|
||||||
const [columnVisibility, setColumnVisibility] =
|
|
||||||
useState<VisibilityState>(hiddenColumn_)
|
|
||||||
const [rowSelection, setRowSelection] = useState({})
|
|
||||||
|
|
||||||
const table = useReactTable({
|
|
||||||
data,
|
|
||||||
columns,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
onRowSelectionChange: setRowSelection,
|
|
||||||
state: {
|
|
||||||
rowSelection,
|
|
||||||
columnVisibility,
|
|
||||||
pagination: {
|
|
||||||
pageIndex,
|
|
||||||
pageSize
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onColumnVisibilityChange: setColumnVisibility,
|
|
||||||
onPaginationChange: (updater) => {
|
|
||||||
const newState =
|
|
||||||
typeof updater === 'function'
|
|
||||||
? updater({ pageIndex, pageSize })
|
|
||||||
: updater
|
|
||||||
|
|
||||||
setSearchParams((searchParams) => {
|
|
||||||
searchParams.set('p', newState?.pageIndex.toString())
|
|
||||||
searchParams.set('perPage', newState?.pageSize.toString())
|
|
||||||
return searchParams
|
|
||||||
})
|
|
||||||
},
|
|
||||||
manualPagination: true,
|
|
||||||
rowCount
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableContext value={{ table }}>
|
|
||||||
<div className="space-y-2.5 max-md:mb-2">
|
|
||||||
<Card className="relative w-full overflow-auto">
|
|
||||||
<CardContent>
|
|
||||||
{children}
|
|
||||||
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
|
||||||
<TableRow
|
|
||||||
key={headerGroup.id}
|
|
||||||
className="hover:bg-transparent"
|
|
||||||
>
|
|
||||||
{headerGroup.headers.map((header) => {
|
|
||||||
return (
|
|
||||||
<TableHead key={header.id} className="p-4">
|
|
||||||
{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} className="p-4">
|
|
||||||
{flexRender(
|
|
||||||
cell.column.columnDef.cell,
|
|
||||||
cell.getContext()
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
))}
|
|
||||||
</TableRow>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell
|
|
||||||
colSpan={columns.length}
|
|
||||||
className="h-24 text-center"
|
|
||||||
>
|
|
||||||
Nenhum resultado.
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
)}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-end gap-5">
|
|
||||||
<div className="hidden items-center gap-2.5 lg:flex">
|
|
||||||
<Label htmlFor="rows-per-page" className="text-sm font-medium">
|
|
||||||
Itens por página
|
|
||||||
</Label>
|
|
||||||
<Select
|
|
||||||
value={String(table.getState().pagination.pageSize)}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
table.setPageSize(Number(value))
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger size="sm" className="w-20">
|
|
||||||
<SelectValue
|
|
||||||
placeholder={String(table.getState().pagination.pageSize)}
|
|
||||||
/>
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent side="top">
|
|
||||||
{[12, 25, 50, 100].map((pageSize) => (
|
|
||||||
<SelectItem key={pageSize} value={String(pageSize)}>
|
|
||||||
{pageSize}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-5">
|
|
||||||
<Pagination>
|
|
||||||
<PaginationContent className="gap-2">
|
|
||||||
<PaginationItem>
|
|
||||||
{(pageIndex + 1) * pageSize - pageSize + 1}-
|
|
||||||
{Math.min((pageIndex + 1) * pageSize, rowCount)}
|
|
||||||
</PaginationItem>
|
|
||||||
|
|
||||||
<PaginationItem>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={() => table.previousPage()}
|
|
||||||
disabled={!table.getCanPreviousPage()}
|
|
||||||
>
|
|
||||||
<ChevronLeftIcon />
|
|
||||||
</Button>
|
|
||||||
</PaginationItem>
|
|
||||||
|
|
||||||
<PaginationItem>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="cursor-pointer"
|
|
||||||
onClick={() => table.nextPage()}
|
|
||||||
disabled={!table.getCanNextPage()}
|
|
||||||
>
|
|
||||||
<ChevronRightIcon />
|
|
||||||
</Button>
|
|
||||||
</PaginationItem>
|
|
||||||
</PaginationContent>
|
|
||||||
</Pagination>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</TableContext>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CustomizeColumns({ className }: { className?: string }) {
|
|
||||||
const { table } = useContext(TableContext)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button variant="outline" className={cn('cursor-pointer', className)}>
|
|
||||||
<Columns2Icon />
|
|
||||||
<span className="hidden lg:inline">Exibir colunas</span>
|
|
||||||
<span className="lg:hidden">Colunas</span>
|
|
||||||
<ChevronDownIcon />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end" className="w-56 *:cursor-pointer">
|
|
||||||
{table
|
|
||||||
.getAllColumns()
|
|
||||||
.filter(
|
|
||||||
(column) =>
|
|
||||||
typeof column.accessorFn !== 'undefined' && column.getCanHide()
|
|
||||||
)
|
|
||||||
.map((column) => {
|
|
||||||
return (
|
|
||||||
<DropdownMenuCheckboxItem
|
|
||||||
key={column.id}
|
|
||||||
checked={column.getIsVisible()}
|
|
||||||
onCheckedChange={(value) => column.toggleVisibility(!!value)}
|
|
||||||
>
|
|
||||||
{column.columnDef.header}
|
|
||||||
</DropdownMenuCheckboxItem>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { type Column } from '@tanstack/react-table'
|
||||||
|
import { ArrowDown, ArrowUp, ChevronsUpDown, EyeOff } from 'lucide-react'
|
||||||
|
|
||||||
|
import { Button } from '@repo/ui/components/ui/button'
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger
|
||||||
|
} from '@repo/ui/components/ui/dropdown-menu'
|
||||||
|
import { cn } from '@repo/ui/lib/utils'
|
||||||
|
|
||||||
|
interface DataTableColumnHeaderProps<TData, TValue>
|
||||||
|
extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
column: Column<TData, TValue>
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DataTableColumnHeader<TData, TValue>({
|
||||||
|
column,
|
||||||
|
title,
|
||||||
|
className
|
||||||
|
}: DataTableColumnHeaderProps<TData, TValue>) {
|
||||||
|
if (!column.getCanSort()) {
|
||||||
|
return <div className={cn(className)}>{title}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('flex items-center gap-2', className)}>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="data-[state=open]:bg-accent -ml-3 h-8"
|
||||||
|
>
|
||||||
|
<span>{title}</span>
|
||||||
|
{column.getIsSorted() === 'desc' ? (
|
||||||
|
<ArrowDown />
|
||||||
|
) : column.getIsSorted() === 'asc' ? (
|
||||||
|
<ArrowUp />
|
||||||
|
) : (
|
||||||
|
<ChevronsUpDown />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start">
|
||||||
|
<DropdownMenuItem onClick={() => column.toggleSorting(false)}>
|
||||||
|
<ArrowUp />
|
||||||
|
Cres.
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => column.toggleSorting(true)}>
|
||||||
|
<ArrowDown />
|
||||||
|
Decr.
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
|
||||||
|
<EyeOff />
|
||||||
|
Ocultar
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import {
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
getSortedRowModel,
|
||||||
|
useReactTable,
|
||||||
|
type ColumnDef,
|
||||||
|
type ColumnSort,
|
||||||
|
type SortingState,
|
||||||
|
type Table,
|
||||||
|
type VisibilityState
|
||||||
|
} from '@tanstack/react-table'
|
||||||
|
import { createContext, useState, type ReactNode } from 'react'
|
||||||
|
import { useSearchParams } from 'react-router'
|
||||||
|
|
||||||
|
import { Card, CardContent } from '@repo/ui/components/ui/card'
|
||||||
|
import {
|
||||||
|
Table as Table_,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow
|
||||||
|
} from '@repo/ui/components/ui/table'
|
||||||
|
import { DataTablePagination } from './pagination'
|
||||||
|
|
||||||
|
interface DataTableProps<TData, TValue> {
|
||||||
|
children?: ReactNode
|
||||||
|
columns: ColumnDef<TData, TValue>[]
|
||||||
|
data: TData[]
|
||||||
|
pageIndex: number
|
||||||
|
sort: SortingState
|
||||||
|
pageSize: number
|
||||||
|
rowCount: number
|
||||||
|
hiddenColumn?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TableContext = createContext<{ table: Table<any> } | null>(null)
|
||||||
|
|
||||||
|
export function DataTable<TData, TValue>({
|
||||||
|
children,
|
||||||
|
columns,
|
||||||
|
data,
|
||||||
|
sort,
|
||||||
|
pageIndex,
|
||||||
|
pageSize,
|
||||||
|
rowCount,
|
||||||
|
hiddenColumn = []
|
||||||
|
}: DataTableProps<TData, TValue>) {
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
|
const hiddenColumn_ = Object.fromEntries(
|
||||||
|
hiddenColumn.map((column) => [column, false])
|
||||||
|
)
|
||||||
|
const [columnVisibility, setColumnVisibility] =
|
||||||
|
useState<VisibilityState>(hiddenColumn_)
|
||||||
|
const [rowSelection, setRowSelection] = useState({})
|
||||||
|
const sortParam = searchParams.get('sort')
|
||||||
|
const sorting = sortParam
|
||||||
|
? sortParam.split(',').map((s) => {
|
||||||
|
const [id, dir] = s.split(':')
|
||||||
|
return { id, desc: dir === 'desc' }
|
||||||
|
})
|
||||||
|
: sort
|
||||||
|
|
||||||
|
const setPagination = (updater: any) => {
|
||||||
|
const newState =
|
||||||
|
typeof updater === 'function' ? updater({ pageIndex, pageSize }) : updater
|
||||||
|
|
||||||
|
setSearchParams((searchParams) => {
|
||||||
|
searchParams.set('p', newState?.pageIndex.toString())
|
||||||
|
searchParams.set('perPage', newState?.pageSize.toString())
|
||||||
|
return searchParams
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const setSorting = (updater: any) => {
|
||||||
|
const newSorting =
|
||||||
|
typeof updater === 'function' ? updater(sorting) : updater
|
||||||
|
|
||||||
|
setSearchParams((searchParams) => {
|
||||||
|
if (newSorting.length) {
|
||||||
|
const sort = newSorting
|
||||||
|
.map((s: ColumnSort) => `${s.id}:${s.desc ? 'desc' : 'asc'}`)
|
||||||
|
.join(',')
|
||||||
|
searchParams.set('sort', sort)
|
||||||
|
}
|
||||||
|
return searchParams
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data,
|
||||||
|
columns,
|
||||||
|
rowCount,
|
||||||
|
state: {
|
||||||
|
sorting,
|
||||||
|
rowSelection,
|
||||||
|
columnVisibility,
|
||||||
|
pagination: {
|
||||||
|
pageIndex,
|
||||||
|
pageSize
|
||||||
|
}
|
||||||
|
},
|
||||||
|
manualSorting: true,
|
||||||
|
manualPagination: true,
|
||||||
|
enableRowSelection: true,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
onRowSelectionChange: setRowSelection,
|
||||||
|
onSortingChange: setSorting,
|
||||||
|
onColumnVisibilityChange: setColumnVisibility,
|
||||||
|
onPaginationChange: setPagination
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableContext value={{ table }}>
|
||||||
|
<div className="space-y-2.5 max-md:mb-2">
|
||||||
|
<Card className="relative w-full overflow-auto">
|
||||||
|
<CardContent>
|
||||||
|
{children}
|
||||||
|
|
||||||
|
<Table_>
|
||||||
|
<TableHeader>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<TableRow
|
||||||
|
key={headerGroup.id}
|
||||||
|
className="hover:bg-transparent"
|
||||||
|
>
|
||||||
|
{headerGroup.headers.map((header) => {
|
||||||
|
return (
|
||||||
|
<TableHead key={header.id} className="p-2.5">
|
||||||
|
{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} className="p-2.5">
|
||||||
|
{flexRender(
|
||||||
|
cell.column.columnDef.cell,
|
||||||
|
cell.getContext()
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={columns.length}
|
||||||
|
className="h-24 text-center"
|
||||||
|
>
|
||||||
|
Nenhum resultado.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table_>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<DataTablePagination table={table} />
|
||||||
|
</div>
|
||||||
|
</TableContext>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export { DataTableColumnHeader } from './column-header'
|
||||||
|
export { DataTable } from './data-table'
|
||||||
|
export { DataTableViewOptions } from './view-options'
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { type Table } from '@tanstack/react-table'
|
||||||
|
import { ChevronLeft, ChevronRight } from 'lucide-react'
|
||||||
|
|
||||||
|
import { Button } from '@repo/ui/components/ui/button'
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue
|
||||||
|
} from '@repo/ui/components/ui/select'
|
||||||
|
|
||||||
|
interface DataTablePaginationProps<TData> {
|
||||||
|
table: Table<TData>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DataTablePagination<TData>({
|
||||||
|
table
|
||||||
|
}: DataTablePaginationProps<TData>) {
|
||||||
|
const { pageIndex, pageSize } = table.getState().pagination
|
||||||
|
const rowCount = table.getRowCount()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="text-muted-foreground flex-1 text-sm">
|
||||||
|
{table.getFilteredSelectedRowModel().rows.length} de{' '}
|
||||||
|
{table.getFilteredRowModel().rows.length} linha(s) selecionada(s).
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3 lg:gap-6">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<p className="text-sm font-medium hidden lg:block">
|
||||||
|
Itens por página
|
||||||
|
</p>
|
||||||
|
<Select
|
||||||
|
value={`${table.getState().pagination.pageSize}`}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
table.setPageSize(Number(value))
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder={table.getState().pagination.pageSize} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent side="top">
|
||||||
|
{[12, 25, 50, 100].map((pageSize) => (
|
||||||
|
<SelectItem key={pageSize} value={`${pageSize}`}>
|
||||||
|
{pageSize}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-sm font-medium">
|
||||||
|
{(pageIndex + 1) * pageSize - pageSize + 1}-
|
||||||
|
{Math.min((pageIndex + 1) * pageSize, rowCount)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 *:cursor-pointer">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="size-8"
|
||||||
|
onClick={() => table.previousPage()}
|
||||||
|
disabled={!table.getCanPreviousPage()}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Ir para a página anterior</span>
|
||||||
|
<ChevronLeft />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="size-8"
|
||||||
|
onClick={() => table.nextPage()}
|
||||||
|
disabled={!table.getCanNextPage()}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Ir para a próxima página</span>
|
||||||
|
<ChevronRight />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useContext } from 'react'
|
||||||
|
|
||||||
|
import { type Table } from '@tanstack/react-table'
|
||||||
|
import { Columns2Icon } from 'lucide-react'
|
||||||
|
|
||||||
|
import { Button } from '@repo/ui/components/ui/button'
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuTrigger
|
||||||
|
} from '@repo/ui/components/ui/dropdown-menu'
|
||||||
|
import { cn } from '@repo/ui/lib/utils'
|
||||||
|
import { TableContext } from './data-table'
|
||||||
|
|
||||||
|
export function DataTableViewOptions<TData>({
|
||||||
|
className
|
||||||
|
}: {
|
||||||
|
className: string
|
||||||
|
}) {
|
||||||
|
const ctx = useContext(TableContext) as { table: Table<TData> } | null
|
||||||
|
|
||||||
|
if (!ctx) {
|
||||||
|
throw new Error('TableContext is null')
|
||||||
|
}
|
||||||
|
|
||||||
|
const { table } = ctx
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="outline" className={cn('cursor-pointer', className)}>
|
||||||
|
<Columns2Icon /> Colunas
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" className="w-44 *:cursor-pointer">
|
||||||
|
{table
|
||||||
|
.getAllColumns()
|
||||||
|
.filter(
|
||||||
|
(column) =>
|
||||||
|
typeof column.accessorFn !== 'undefined' && column.getCanHide()
|
||||||
|
)
|
||||||
|
.map((column) => {
|
||||||
|
return (
|
||||||
|
<DropdownMenuCheckboxItem
|
||||||
|
key={column.id}
|
||||||
|
checked={column.getIsVisible()}
|
||||||
|
onCheckedChange={(value) => column.toggleVisibility(!!value)}
|
||||||
|
>
|
||||||
|
{column.columnDef?.meta?.title ?? column.id}
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -32,7 +32,7 @@ type RangeCalendarFilterProps = {
|
|||||||
|
|
||||||
export function RangeCalendarFilter({
|
export function RangeCalendarFilter({
|
||||||
children,
|
children,
|
||||||
value,
|
value = undefined,
|
||||||
className,
|
className,
|
||||||
onChange
|
onChange
|
||||||
}: RangeCalendarFilterProps) {
|
}: RangeCalendarFilterProps) {
|
||||||
@@ -85,8 +85,8 @@ export function RangeCalendarFilter({
|
|||||||
return setDateRange(undefined)
|
return setDateRange(undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dateRange.to?.getTime() === dateRange.from?.getTime()) {
|
if (dateRange.from?.getTime() === dateRange.to?.getTime()) {
|
||||||
const nextDay = new Date(String(dateRange?.from))
|
const nextDay = new Date(String(dateRange.from))
|
||||||
nextDay.setDate(nextDay.getDate() + 6)
|
nextDay.setDate(nextDay.getDate() + 6)
|
||||||
dateRange.to = nextDay
|
dateRange.to = nextDay
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import type { CellContext, ColumnDef } from '@tanstack/react-table'
|
import type { CellContext, ColumnDef } from '@tanstack/react-table'
|
||||||
import {
|
import { HelpCircleIcon } from 'lucide-react'
|
||||||
CircleCheckIcon,
|
|
||||||
CircleIcon,
|
|
||||||
CircleOffIcon,
|
|
||||||
CircleXIcon,
|
|
||||||
HelpCircleIcon,
|
|
||||||
TimerIcon,
|
|
||||||
type LucideIcon
|
|
||||||
} from 'lucide-react'
|
|
||||||
|
|
||||||
import { Avatar, AvatarFallback } from '@repo/ui/components/ui/avatar'
|
import { Avatar, AvatarFallback } from '@repo/ui/components/ui/avatar'
|
||||||
import { Badge } from '@repo/ui/components/ui/badge'
|
import { Badge } from '@repo/ui/components/ui/badge'
|
||||||
@@ -17,6 +9,9 @@ import { Checkbox } from '@repo/ui/components/ui/checkbox'
|
|||||||
import { Progress } from '@repo/ui/components/ui/progress'
|
import { Progress } from '@repo/ui/components/ui/progress'
|
||||||
import { cn, initials } from '@repo/ui/lib/utils'
|
import { cn, initials } from '@repo/ui/lib/utils'
|
||||||
|
|
||||||
|
import { DataTableColumnHeader } from '@/components/data-table/column-header'
|
||||||
|
import { labels, statuses } from './data'
|
||||||
|
|
||||||
// This type is used to define the shape of our data.
|
// This type is used to define the shape of our data.
|
||||||
// You can use a Zod schema here if you want.
|
// You can use a Zod schema here if you want.
|
||||||
type Course = {
|
type Course = {
|
||||||
@@ -41,48 +36,6 @@ const formatted = new Intl.DateTimeFormat('pt-BR', {
|
|||||||
minute: '2-digit'
|
minute: '2-digit'
|
||||||
})
|
})
|
||||||
|
|
||||||
export const statuses: Record<
|
|
||||||
string,
|
|
||||||
{ icon: LucideIcon; color?: string; label: string }
|
|
||||||
> = {
|
|
||||||
PENDING: {
|
|
||||||
icon: CircleIcon,
|
|
||||||
label: 'Não iniciado'
|
|
||||||
},
|
|
||||||
IN_PROGRESS: {
|
|
||||||
icon: TimerIcon,
|
|
||||||
color: 'text-blue-400 [&_svg]:text-blue-500',
|
|
||||||
label: 'Em andamento'
|
|
||||||
},
|
|
||||||
COMPLETED: {
|
|
||||||
icon: CircleCheckIcon,
|
|
||||||
color: 'text-green-400 [&_svg]:text-background [&_svg]:fill-green-500',
|
|
||||||
label: 'Aprovado'
|
|
||||||
},
|
|
||||||
FAILED: {
|
|
||||||
icon: CircleXIcon,
|
|
||||||
color: 'text-red-400 [&_svg]:text-red-500',
|
|
||||||
label: 'Reprovado'
|
|
||||||
},
|
|
||||||
CANCELED: {
|
|
||||||
icon: CircleOffIcon,
|
|
||||||
color: 'text-orange-400 [&_svg]:text-orange-500',
|
|
||||||
label: 'Cancelado'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultIcon = {
|
|
||||||
icon: HelpCircleIcon
|
|
||||||
}
|
|
||||||
|
|
||||||
const statusTranslate: Record<string, string> = {
|
|
||||||
PENDING: 'Não iniciado',
|
|
||||||
IN_PROGRESS: 'Em andamento',
|
|
||||||
COMPLETED: 'Aprovado',
|
|
||||||
FAILED: 'Reprovado',
|
|
||||||
CANCELED: 'Cancelado'
|
|
||||||
}
|
|
||||||
|
|
||||||
export const columns: ColumnDef<Enrollment>[] = [
|
export const columns: ColumnDef<Enrollment>[] = [
|
||||||
{
|
{
|
||||||
id: 'select',
|
id: 'select',
|
||||||
@@ -105,10 +58,11 @@ export const columns: ColumnDef<Enrollment>[] = [
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
accessorKey: 'user',
|
||||||
header: 'Colaborador',
|
header: 'Colaborador',
|
||||||
enableHiding: false,
|
enableHiding: false,
|
||||||
cell: ({ row: { original } }) => {
|
cell: ({ row }) => {
|
||||||
const { user } = original
|
const user = row.getValue('user') as { name: string; email: string }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-2.5 items-center">
|
<div className="flex gap-2.5 items-center">
|
||||||
@@ -127,22 +81,27 @@ export const columns: ColumnDef<Enrollment>[] = [
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'course.name',
|
accessorKey: 'course',
|
||||||
header: 'Curso',
|
header: 'Curso',
|
||||||
enableHiding: false,
|
enableHiding: false,
|
||||||
cell: ({ row: { original } }) => (
|
cell: ({ row }) => {
|
||||||
<abbr className="truncate max-w-62 block" title={original.course.name}>
|
const { name } = row.getValue('course') as { name: string }
|
||||||
{original.course.name}
|
|
||||||
|
return (
|
||||||
|
<abbr className="truncate max-w-62 block" title={name}>
|
||||||
|
{name}
|
||||||
</abbr>
|
</abbr>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'status',
|
accessorKey: 'status',
|
||||||
header: 'Status',
|
header: 'Status',
|
||||||
enableHiding: false,
|
enableHiding: false,
|
||||||
cell: ({ row: { original } }) => {
|
cell: ({ row }) => {
|
||||||
const status = statusTranslate[original.status] ?? original.status
|
const s = row.getValue('status') as string
|
||||||
const { icon: Icon, color } = statuses?.[original.status] ?? defaultIcon
|
const status = labels[s] ?? s
|
||||||
|
const { icon: Icon, color } = statuses?.[s] ?? { icon: HelpCircleIcon }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Badge variant="outline" className={cn(color, ' px-1.5')}>
|
<Badge variant="outline" className={cn(color, ' px-1.5')}>
|
||||||
@@ -156,44 +115,63 @@ export const columns: ColumnDef<Enrollment>[] = [
|
|||||||
accessorKey: 'progress',
|
accessorKey: 'progress',
|
||||||
header: 'Progresso',
|
header: 'Progresso',
|
||||||
enableHiding: false,
|
enableHiding: false,
|
||||||
cell: ({ row: { original } }) => (
|
cell: ({ row }) => {
|
||||||
|
const progress = row.getValue('progress')
|
||||||
|
|
||||||
|
return (
|
||||||
<div className="flex gap-2.5 items-center ">
|
<div className="flex gap-2.5 items-center ">
|
||||||
<Progress value={Number(original.progress)} className="w-32" />
|
<Progress value={Number(progress)} className="w-32" />
|
||||||
<span className="text-xs">{original.progress}%</span>
|
<span className="text-xs">{String(progress)}%</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'created_at',
|
accessorKey: 'created_at',
|
||||||
header: 'Matriculado em',
|
header: ({ column }) => (
|
||||||
|
<DataTableColumnHeader column={column} title="Matriculado em" />
|
||||||
|
),
|
||||||
|
meta: { title: 'Matriculado em' },
|
||||||
enableSorting: true,
|
enableSorting: true,
|
||||||
enableHiding: true,
|
enableHiding: true,
|
||||||
cell: cellDate
|
cell: cellDate
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'started_at',
|
accessorKey: 'started_at',
|
||||||
header: 'Iniciado em',
|
header: ({ column }) => (
|
||||||
|
<DataTableColumnHeader column={column} title="Iniciado em" />
|
||||||
|
),
|
||||||
|
meta: { title: 'Iniciado em' },
|
||||||
enableSorting: true,
|
enableSorting: true,
|
||||||
enableHiding: true,
|
enableHiding: true,
|
||||||
cell: cellDate
|
cell: cellDate
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'completed_at',
|
accessorKey: 'completed_at',
|
||||||
header: 'Aprovado em',
|
header: ({ column }) => (
|
||||||
|
<DataTableColumnHeader column={column} title="Aprovado em" />
|
||||||
|
),
|
||||||
|
meta: { title: 'Aprovado em' },
|
||||||
enableSorting: true,
|
enableSorting: true,
|
||||||
enableHiding: true,
|
enableHiding: true,
|
||||||
cell: cellDate
|
cell: cellDate
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'failed_at',
|
accessorKey: 'failed_at',
|
||||||
header: 'Reprovado em',
|
header: ({ column }) => (
|
||||||
|
<DataTableColumnHeader column={column} title="Reprovado em" />
|
||||||
|
),
|
||||||
|
meta: { title: 'Reprovado em' },
|
||||||
enableSorting: true,
|
enableSorting: true,
|
||||||
enableHiding: true,
|
enableHiding: true,
|
||||||
cell: cellDate
|
cell: cellDate
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'canceled_at',
|
accessorKey: 'canceled_at',
|
||||||
header: 'Cancelado em',
|
header: ({ column }) => (
|
||||||
|
<DataTableColumnHeader column={column} title="Cancelado em" />
|
||||||
|
),
|
||||||
|
meta: { title: 'Cancelado em' },
|
||||||
enableSorting: true,
|
enableSorting: true,
|
||||||
enableHiding: true,
|
enableHiding: true,
|
||||||
cell: cellDate
|
cell: cellDate
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import {
|
||||||
|
CircleCheckIcon,
|
||||||
|
CircleIcon,
|
||||||
|
CircleOffIcon,
|
||||||
|
CircleXIcon,
|
||||||
|
TimerIcon,
|
||||||
|
type LucideIcon
|
||||||
|
} from 'lucide-react'
|
||||||
|
|
||||||
|
export const statuses: Record<
|
||||||
|
string,
|
||||||
|
{ icon: LucideIcon; color?: string; label: string }
|
||||||
|
> = {
|
||||||
|
PENDING: {
|
||||||
|
icon: CircleIcon,
|
||||||
|
label: 'Não iniciado'
|
||||||
|
},
|
||||||
|
IN_PROGRESS: {
|
||||||
|
icon: TimerIcon,
|
||||||
|
color: 'text-blue-400 [&_svg]:text-blue-500',
|
||||||
|
label: 'Em andamento'
|
||||||
|
},
|
||||||
|
COMPLETED: {
|
||||||
|
icon: CircleCheckIcon,
|
||||||
|
color: 'text-green-400 [&_svg]:text-background [&_svg]:fill-green-500',
|
||||||
|
label: 'Aprovado'
|
||||||
|
},
|
||||||
|
FAILED: {
|
||||||
|
icon: CircleXIcon,
|
||||||
|
color: 'text-red-400 [&_svg]:text-red-500',
|
||||||
|
label: 'Reprovado'
|
||||||
|
},
|
||||||
|
CANCELED: {
|
||||||
|
icon: CircleOffIcon,
|
||||||
|
color: 'text-orange-400 [&_svg]:text-orange-500',
|
||||||
|
label: 'Cancelado'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const labels: Record<string, string> = {
|
||||||
|
PENDING: 'Não iniciado',
|
||||||
|
IN_PROGRESS: 'Em andamento',
|
||||||
|
COMPLETED: 'Aprovado',
|
||||||
|
FAILED: 'Reprovado',
|
||||||
|
CANCELED: 'Cancelado'
|
||||||
|
}
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
import type { Route } from './+types'
|
import type { Route } from './+types'
|
||||||
|
|
||||||
import { PlusCircleIcon, PlusIcon } from 'lucide-react'
|
import { PlusCircleIcon, PlusIcon } from 'lucide-react'
|
||||||
import { DateTime } from 'luxon'
|
|
||||||
import { MeiliSearchFilterBuilder } from 'meilisearch-helper'
|
import { MeiliSearchFilterBuilder } from 'meilisearch-helper'
|
||||||
import { Suspense, useState } from 'react'
|
import { Suspense, useState } from 'react'
|
||||||
import { Await, Link, useSearchParams } from 'react-router'
|
import { Await, Link, useSearchParams } from 'react-router'
|
||||||
|
|
||||||
import { CustomizeColumns, DataTable } from '@/components/data-table'
|
import { DataTable, DataTableViewOptions } from '@/components/data-table'
|
||||||
import { RangeCalendarFilter } from '@/components/range-calendar-filter'
|
import { RangeCalendarFilter } from '@/components/range-calendar-filter'
|
||||||
import { createSearch } from '@/lib/meili'
|
import { createSearch } from '@/lib/meili'
|
||||||
|
|
||||||
@@ -23,23 +22,20 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue
|
SelectValue
|
||||||
} from '@repo/ui/components/ui/select'
|
} from '@repo/ui/components/ui/select'
|
||||||
import { columns, statuses, type Enrollment } from './columns'
|
import { columns, type Enrollment } from './columns'
|
||||||
|
import { statuses } from './data'
|
||||||
|
|
||||||
export function meta({}: Route.MetaArgs) {
|
export function meta({}: Route.MetaArgs) {
|
||||||
return [{ title: 'Matrículas' }]
|
return [{ title: 'Matrículas' }]
|
||||||
}
|
}
|
||||||
|
|
||||||
const dtOptions = {
|
|
||||||
zone: 'America/Sao_Paulo'
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loader({ params, context, request }: Route.LoaderArgs) {
|
export async function loader({ params, context, request }: Route.LoaderArgs) {
|
||||||
const { searchParams } = new URL(request.url)
|
const { searchParams } = new URL(request.url)
|
||||||
const { orgid } = params
|
const { orgid } = params
|
||||||
const query = searchParams.get('q') || ''
|
const query = searchParams.get('q') || ''
|
||||||
const field = searchParams.get('field') || ''
|
|
||||||
const from = searchParams.get('from') || ''
|
const from = searchParams.get('from') || ''
|
||||||
const to = searchParams.get('to') || ''
|
const to = searchParams.get('to') || ''
|
||||||
|
const sort = searchParams.get('sort') || 'created_at:desc'
|
||||||
const status = searchParams.getAll('status') || []
|
const status = searchParams.getAll('status') || []
|
||||||
const page = Number(searchParams.get('p')) + 1
|
const page = Number(searchParams.get('p')) + 1
|
||||||
const hitsPerPage = Number(searchParams.get('perPage')) || 25
|
const hitsPerPage = Number(searchParams.get('perPage')) || 25
|
||||||
@@ -50,15 +46,16 @@ export async function loader({ params, context, request }: Route.LoaderArgs) {
|
|||||||
builder = builder.where('status', 'in', status)
|
builder = builder.where('status', 'in', status)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (field && from && to) {
|
if (from && to) {
|
||||||
builder = builder.where(field, 'between', [from, to])
|
const [field, from_] = from.split(':')
|
||||||
|
builder = builder.where(field, 'between', [from_, to])
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: createSearch({
|
data: createSearch({
|
||||||
index: 'betaeducacao-prod-enrollments',
|
index: 'betaeducacao-prod-enrollments',
|
||||||
sort: ['created_at:desc'],
|
|
||||||
filter: builder.build(),
|
filter: builder.build(),
|
||||||
|
sort: [sort],
|
||||||
query,
|
query,
|
||||||
page,
|
page,
|
||||||
hitsPerPage,
|
hitsPerPage,
|
||||||
@@ -75,10 +72,8 @@ const formatted = new Intl.DateTimeFormat('en-CA', {
|
|||||||
|
|
||||||
export default function Route({ loaderData: { data } }) {
|
export default function Route({ loaderData: { data } }) {
|
||||||
const [searchParams, setSearchParams] = useSearchParams()
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
const [from, to] = [searchParams.get('from'), searchParams.get('to')]
|
// const [rangeField, setRangeField] = useState<string>('created_at')
|
||||||
const [rangeField, setRangeField] = useState<string>(
|
const { rangeField, dateRange } = useRangeParams('created_at')
|
||||||
searchParams.get('field') || 'created_at'
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<Skeleton />}>
|
<Suspense fallback={<Skeleton />}>
|
||||||
@@ -95,6 +90,7 @@ export default function Route({ loaderData: { data } }) {
|
|||||||
{({ hits, page, hitsPerPage, totalHits }) => {
|
{({ hits, page, hitsPerPage, totalHits }) => {
|
||||||
return (
|
return (
|
||||||
<DataTable
|
<DataTable
|
||||||
|
sort={[{ id: 'created_at', desc: true }]}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={hits as Enrollment[]}
|
data={hits as Enrollment[]}
|
||||||
pageIndex={page - 1}
|
pageIndex={page - 1}
|
||||||
@@ -158,21 +154,13 @@ export default function Route({ loaderData: { data } }) {
|
|||||||
|
|
||||||
<RangeCalendarFilter
|
<RangeCalendarFilter
|
||||||
className="lg:flex-1"
|
className="lg:flex-1"
|
||||||
value={
|
value={dateRange}
|
||||||
from && to
|
|
||||||
? {
|
|
||||||
from: DateTime.fromISO(from, dtOptions),
|
|
||||||
to: DateTime.fromISO(to, dtOptions)
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
onChange={(dateRange) => {
|
onChange={(dateRange) => {
|
||||||
setSearchParams((searchParams) => {
|
setSearchParams((searchParams) => {
|
||||||
if (dateRange) {
|
if (dateRange) {
|
||||||
searchParams.set('field', rangeField)
|
|
||||||
searchParams.set(
|
searchParams.set(
|
||||||
'from',
|
'from',
|
||||||
formatted.format(dateRange.from)
|
`${rangeField}:${formatted.format(dateRange.from)}`
|
||||||
)
|
)
|
||||||
searchParams.set(
|
searchParams.set(
|
||||||
'to',
|
'to',
|
||||||
@@ -181,7 +169,6 @@ export default function Route({ loaderData: { data } }) {
|
|||||||
} else {
|
} else {
|
||||||
searchParams.delete('from')
|
searchParams.delete('from')
|
||||||
searchParams.delete('to')
|
searchParams.delete('to')
|
||||||
searchParams.delete('field')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return searchParams
|
return searchParams
|
||||||
@@ -191,11 +178,13 @@ export default function Route({ loaderData: { data } }) {
|
|||||||
<div className="p-2.5">
|
<div className="p-2.5">
|
||||||
<Select
|
<Select
|
||||||
defaultValue={rangeField}
|
defaultValue={rangeField}
|
||||||
onValueChange={(field) => {
|
onValueChange={(value) => {
|
||||||
setRangeField(field)
|
|
||||||
setSearchParams((searchParams) => {
|
setSearchParams((searchParams) => {
|
||||||
if (searchParams.has('from')) {
|
if (searchParams.has('from')) {
|
||||||
searchParams.set('field', field)
|
searchParams.set(
|
||||||
|
'from',
|
||||||
|
`${value}:${dateRange?.from?.getTime()}`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return searchParams
|
return searchParams
|
||||||
@@ -230,7 +219,7 @@ export default function Route({ loaderData: { data } }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="lg:ml-auto flex gap-2.5">
|
<div className="lg:ml-auto flex gap-2.5">
|
||||||
<CustomizeColumns className="flex-1" />
|
<DataTableViewOptions className="flex-1" />
|
||||||
|
|
||||||
<Button className="flex-1" asChild>
|
<Button className="flex-1" asChild>
|
||||||
<Link to="add">
|
<Link to="add">
|
||||||
@@ -247,3 +236,22 @@ export default function Route({ loaderData: { data } }) {
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useRangeParams(initRangeField: string) {
|
||||||
|
const [searchParams] = useSearchParams()
|
||||||
|
const [from, to] = [searchParams.get('from'), searchParams.get('to')]
|
||||||
|
|
||||||
|
if (!from || !to) {
|
||||||
|
return { rangeField: initRangeField, dateRange: undefined }
|
||||||
|
}
|
||||||
|
|
||||||
|
const [rangeField, from_] = from.split(':')
|
||||||
|
|
||||||
|
return {
|
||||||
|
rangeField,
|
||||||
|
dateRange: {
|
||||||
|
from: new Date(from_),
|
||||||
|
to: new Date(to)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"cookie": "^1.0.2",
|
"cookie": "^1.0.2",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
"file-saver": "^2.0.5",
|
||||||
"fuse.js": "^7.1.0",
|
"fuse.js": "^7.1.0",
|
||||||
"http-status-codes": "^2.3.0",
|
"http-status-codes": "^2.3.0",
|
||||||
"isbot": "^5.1.31",
|
"isbot": "^5.1.31",
|
||||||
@@ -27,6 +28,7 @@
|
|||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-router": "^7.9.5",
|
"react-router": "^7.9.5",
|
||||||
|
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -116,10 +116,10 @@ export default function Component({
|
|||||||
<div className="flex gap-2.5">
|
<div className="flex gap-2.5">
|
||||||
<div className="w-full xl:w-93">
|
<div className="w-full xl:w-93">
|
||||||
<SearchForm
|
<SearchForm
|
||||||
defaultValue={searchParams.get('term') || ''}
|
defaultValue={term || ''}
|
||||||
placeholder={
|
placeholder={
|
||||||
<>
|
<>
|
||||||
Pressione <Kbd>/</Kbd> para filtrar...
|
Digite <Kbd>/</Kbd> para pesquisar
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
@@ -159,7 +159,7 @@ export default function Component({
|
|||||||
<div className="grid lg:grid-cols-4 gap-5">
|
<div className="grid lg:grid-cols-4 gap-5">
|
||||||
<Await resolve={data}>
|
<Await resolve={data}>
|
||||||
{({ hits = [] }) => {
|
{({ hits = [] }) => {
|
||||||
return <List term={term} hits={hits} />
|
return <List term={term} hits={hits as Enrollment[]} />
|
||||||
}}
|
}}
|
||||||
</Await>
|
</Await>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,4 +14,4 @@ OAUTH2_SCOPES_SUPPORTED: list[str] = [
|
|||||||
'apps:insights',
|
'apps:insights',
|
||||||
]
|
]
|
||||||
|
|
||||||
SESSION_EXPIRES_IN = 86400 * 30 # 30 days
|
SESSION_EXPIRES_IN = 86_400 * 30 # 30 days
|
||||||
|
|||||||
20
package-lock.json
generated
20
package-lock.json
generated
@@ -31,6 +31,7 @@
|
|||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"cookie": "^1.0.2",
|
"cookie": "^1.0.2",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
"file-saver": "^2.0.5",
|
||||||
"fuse.js": "^7.1.0",
|
"fuse.js": "^7.1.0",
|
||||||
"http-status-codes": "^2.3.0",
|
"http-status-codes": "^2.3.0",
|
||||||
"isbot": "^5.1.31",
|
"isbot": "^5.1.31",
|
||||||
@@ -40,6 +41,7 @@
|
|||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-router": "^7.9.5",
|
"react-router": "^7.9.5",
|
||||||
|
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -5332,6 +5334,12 @@
|
|||||||
"integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==",
|
"integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/file-saver": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/foreground-child": {
|
"node_modules/foreground-child": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||||
@@ -8077,6 +8085,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/xlsx": {
|
||||||
|
"version": "0.20.3",
|
||||||
|
"resolved": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
|
||||||
|
"integrity": "sha512-oLDq3jw7AcLqKWH2AhCpVTZl8mf6X2YReP+Neh0SJUzV/BdZYjth94tG5toiMB1PPrYtxOCfaoUCkvtuH+3AJA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"xlsx": "bin/xlsx.njs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yallist": {
|
"node_modules/yallist": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ import { debounce } from 'lodash'
|
|||||||
import { SearchIcon, XIcon } from 'lucide-react'
|
import { SearchIcon, XIcon } from 'lucide-react'
|
||||||
import { useRef } from 'react'
|
import { useRef } from 'react'
|
||||||
|
|
||||||
|
import { cn } from '../lib/utils'
|
||||||
import {
|
import {
|
||||||
InputGroup,
|
InputGroup,
|
||||||
InputGroupAddon,
|
InputGroupAddon,
|
||||||
InputGroupButton,
|
InputGroupButton,
|
||||||
InputGroupInput
|
InputGroupInput
|
||||||
} from '@repo/ui/components/ui/input-group'
|
} from './ui/input-group'
|
||||||
import { cn } from '@repo/ui/lib/utils'
|
|
||||||
|
|
||||||
export function SearchForm({
|
export function SearchForm({
|
||||||
placeholder,
|
placeholder,
|
||||||
|
|||||||
Reference in New Issue
Block a user