This commit is contained in:
2025-11-10 00:48:39 -03:00
parent 24dfefe395
commit c3f370e86c
3 changed files with 152 additions and 149 deletions

View File

@@ -6,6 +6,7 @@ import {
useReactTable, useReactTable,
type ColumnDef, type ColumnDef,
type ColumnSort, type ColumnSort,
type RowSelectionState,
type SortingState, type SortingState,
type Table, type Table,
type VisibilityState type VisibilityState
@@ -34,7 +35,7 @@ interface DataTableProps<TData, TValue> {
children?: ReactNode children?: ReactNode
columns: ColumnDef<TData, TValue>[] columns: ColumnDef<TData, TValue>[]
data: TData[] data: TData[]
onRowSelectionChange?: (rowSelection: TData[]) => void setSelectedRows?: (selectedRows: TData[]) => void
pageIndex: number pageIndex: number
sort: SortingState sort: SortingState
pageSize: number pageSize: number
@@ -62,16 +63,17 @@ export function DataTable<TData, TValue>({
pageIndex, pageIndex,
pageSize, pageSize,
rowCount, rowCount,
onRowSelectionChange, setSelectedRows,
hiddenColumn = [] hiddenColumn = []
}: DataTableProps<TData, TValue>) { }: DataTableProps<TData, TValue>) {
const [searchParams, setSearchParams] = useSearchParams() const columnVisibilityInit = Object.fromEntries(
const hiddenColumn_ = Object.fromEntries(
hiddenColumn.map((column) => [column, false]) hiddenColumn.map((column) => [column, false])
) )
const [searchParams, setSearchParams] = useSearchParams()
const [columnVisibility, setColumnVisibility] = const [columnVisibility, setColumnVisibility] =
useState<VisibilityState>(hiddenColumn_) useState<VisibilityState>(columnVisibilityInit)
const [rowSelection, setRowSelection] = useState({}) const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
const sortParam = searchParams.get('sort') const sortParam = searchParams.get('sort')
const sorting = sortParam const sorting = sortParam
? sortParam.split(',').map((s) => { ? sortParam.split(',').map((s) => {
@@ -84,9 +86,7 @@ export function DataTable<TData, TValue>({
const newState = const newState =
typeof updater === 'function' ? updater({ pageIndex, pageSize }) : updater typeof updater === 'function' ? updater({ pageIndex, pageSize }) : updater
onRowSelectionChange?.([])
setRowSelection({}) setRowSelection({})
setSearchParams((searchParams) => { setSearchParams((searchParams) => {
searchParams.set('p', newState?.pageIndex.toString()) searchParams.set('p', newState?.pageIndex.toString())
searchParams.set('perPage', newState?.pageSize.toString()) searchParams.set('perPage', newState?.pageSize.toString())
@@ -135,8 +135,8 @@ export function DataTable<TData, TValue>({
useEffect(() => { useEffect(() => {
const selected = table.getSelectedRowModel().flatRows.map((r) => r.original) const selected = table.getSelectedRowModel().flatRows.map((r) => r.original)
onRowSelectionChange?.(selected) setSelectedRows?.(selected)
}, [rowSelection, table]) }, [rowSelection])
return ( return (
<TableContext value={{ table }}> <TableContext value={{ table }}>

View File

@@ -52,3 +52,18 @@ export const sortings: Record<string, string> = {
failed_at: 'Reprovado em', failed_at: 'Reprovado em',
canceled_at: 'Cancelado em' canceled_at: 'Cancelado em'
} }
export const headers = {
id: 'ID',
'user.name': 'Nome',
'user.email': 'Email',
'user.cpf': 'CPF',
'course.name': 'Curso',
status: 'Status',
progress: 'Progresso',
created_at: 'Cadastrado em',
started_at: 'Iniciado em',
completed_at: 'Concluído em',
failed_at: 'Reprovado em',
canceled_at: 'Cancelado em'
}

View File

@@ -34,7 +34,7 @@ import {
} from '@repo/ui/components/ui/dropdown-menu' } from '@repo/ui/components/ui/dropdown-menu'
import { Kbd } from '@repo/ui/components/ui/kbd' import { Kbd } from '@repo/ui/components/ui/kbd'
import { columns, type Enrollment } from './columns' import { columns, type Enrollment } from './columns'
import { sortings, statuses } from './data' import { headers, sortings, statuses } from './data'
export function meta({}: Route.MetaArgs) { export function meta({}: Route.MetaArgs) {
return [{ title: 'Matrículas' }] return [{ title: 'Matrículas' }]
@@ -83,7 +83,7 @@ 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 [rowSelection, setRowSelection] = useState<Enrollment[]>([]) const [selectedRows, setSelectedRows] = useState<Enrollment[]>([])
const status = searchParams.get('status') const status = searchParams.get('status')
const rangeParams = useRangeParams() const rangeParams = useRangeParams()
@@ -99,133 +99,134 @@ export default function Route({ loaderData: { data } }) {
</div> </div>
<Await resolve={data}> <Await resolve={data}>
{({ hits, page, hitsPerPage, totalHits }) => { {({ hits, page, hitsPerPage, totalHits }) => (
return ( <DataTable
<DataTable sort={[{ id: 'created_at', desc: true }]}
sort={[{ id: 'created_at', desc: true }]} columns={columns}
columns={columns} data={hits as Enrollment[]}
data={hits as Enrollment[]} pageIndex={page - 1}
pageIndex={page - 1} pageSize={hitsPerPage}
pageSize={hitsPerPage} setSelectedRows={setSelectedRows}
onRowSelectionChange={setRowSelection} rowCount={totalHits}
rowCount={totalHits} hiddenColumn={[
hiddenColumn={[ 'completed_at',
'completed_at', 'started_at',
'started_at', 'failed_at',
'failed_at', 'canceled_at'
'canceled_at' ]}
]} >
> <div className="flex gap-2.5 mb-2.5">
<div className="flex gap-2.5 mb-2.5"> {selectedRows.length ? (
{rowSelection.length ? ( <>
<> <div className="flex gap-2.5 items-center">
<div className="flex gap-2.5 items-center"> <Button variant="outline">
<Button variant="outline"> <TagIcon /> Marcador
<TagIcon /> Marcador </Button>
</Button> <DropdownMenuExport
<DropdownMenuExport rowSelection={rowSelection} /> headers={headers}
</div> selectedRows={selectedRows}
</> />
) : ( </div>
<> </>
<div className="w-full 2xl:w-1/3"> ) : (
<SearchForm <>
defaultValue={searchParams.get('q') || ''} <div className="w-full 2xl:w-1/3">
placeholder={ <SearchForm
<> defaultValue={searchParams.get('q') || ''}
Digite <Kbd className="border font-mono">/</Kbd>{' '} placeholder={
para pesquisar <>
</> Digite <Kbd className="border font-mono">/</Kbd> para
} pesquisar
onChange={(value) => </>
}
onChange={(value) =>
setSearchParams((searchParams) => {
searchParams.set('q', String(value))
searchParams.delete('p')
return searchParams
})
}
/>
</div>
<div className="flex gap-2.5 max-lg:flex-col w-full">
<div className="flex gap-2.5 max-lg:flex-col">
<FacetedFilter
title="Status"
icon={PlusCircleIcon}
className="lg:flex-1"
value={status ? status.split(',') : []}
onChange={(statuses) => {
setSearchParams((searchParams) => { setSearchParams((searchParams) => {
searchParams.set('q', String(value)) searchParams.delete('status')
searchParams.delete('p') searchParams.delete('p')
if (statuses.length) {
searchParams.set('status', statuses.join(','))
}
return searchParams return searchParams
}) })
} }}
options={Object.entries(statuses).map(
([key, value]) => ({
value: key,
...value
})
)}
/>
<RangeCalendarFilter
title="Período"
icon={CalendarIcon}
value={rangeParams}
className="lg:flex-1"
options={Object.entries(sortings).map(
([value, label]) => ({
value,
label
})
)}
onChange={(props) => {
setSearchParams((searchParams) => {
if (!props) {
searchParams.delete('from')
searchParams.delete('to')
return searchParams
}
const { rangeField, dateRange } = props
searchParams.set(
'from',
`${rangeField}:${formatted.format(dateRange?.from)}`
)
searchParams.set(
'to',
formatted.format(dateRange?.to)
)
return searchParams
})
}}
/> />
</div> </div>
<div className="flex gap-2.5 max-lg:flex-col w-full"> <div className="lg:ml-auto flex gap-2.5">
<div className="flex gap-2.5 max-lg:flex-col"> <DataTableViewOptions className="flex-1" />
<FacetedFilter
title="Status"
icon={PlusCircleIcon}
className="lg:flex-1"
value={status ? status.split(',') : []}
onChange={(statuses) => {
setSearchParams((searchParams) => {
searchParams.delete('status')
searchParams.delete('p')
if (statuses.length) { <Button className="flex-1" asChild>
searchParams.set('status', statuses.join(',')) <Link to="add">
} <PlusIcon /> Adicionar
</Link>
return searchParams </Button>
})
}}
options={Object.entries(statuses).map(
([key, value]) => ({
value: key,
...value
})
)}
/>
<RangeCalendarFilter
title="Período"
icon={CalendarIcon}
value={rangeParams}
className="lg:flex-1"
options={Object.entries(sortings).map(
([value, label]) => ({
value,
label
})
)}
onChange={(props) => {
setSearchParams((searchParams) => {
if (!props) {
searchParams.delete('from')
searchParams.delete('to')
return searchParams
}
const { rangeField, dateRange } = props
searchParams.set(
'from',
`${rangeField}:${formatted.format(dateRange?.from)}`
)
searchParams.set(
'to',
formatted.format(dateRange?.to)
)
return searchParams
})
}}
/>
</div>
<div className="lg:ml-auto flex gap-2.5">
<DataTableViewOptions className="flex-1" />
<Button className="flex-1" asChild>
<Link to="add">
<PlusIcon /> Adicionar
</Link>
</Button>
</div>
</div> </div>
</> </div>
)} </>
</div> )}
</DataTable> </div>
) </DataTable>
}} )}
</Await> </Await>
</Suspense> </Suspense>
) )
@@ -251,32 +252,19 @@ function useRangeParams() {
} }
export function DropdownMenuExport({ export function DropdownMenuExport({
rowSelection = [] headers,
selectedRows = []
}: { }: {
rowSelection: object[] headers: Record<string, string>
selectedRows: object[]
}) { }) {
const headers = {
id: 'ID',
'user.name': 'Nome',
'user.email': 'Email',
'user.cpf': 'CPF',
'course.name': 'Curso',
status: 'Status',
progress: 'Progresso',
created_at: 'Cadastrado em',
started_at: 'Iniciado em',
completed_at: 'Concluído em',
failed_at: 'Reprovado em',
canceled_at: 'Cancelado em'
}
const handleExport = (bookType: BookType) => () => { const handleExport = (bookType: BookType) => () => {
if (!rowSelection.length) { if (!selectedRows.length) {
return return
} }
const header = Object.keys(headers) const header = Object.keys(headers)
const data = rowSelection.map((data) => { const data = selectedRows.map((data) => {
const obj: Record<string, string> = flatten(data) const obj: Record<string, string> = flatten(data)
return Object.fromEntries(header.map((k) => [k, obj?.[k]])) return Object.fromEntries(header.map((k) => [k, obj?.[k]]))
}) })