update
This commit is contained in:
@@ -1,15 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import type { CellContext, ColumnDef } from '@tanstack/react-table'
|
||||
import {
|
||||
CircleCheckIcon,
|
||||
CircleIcon,
|
||||
CircleOffIcon,
|
||||
CircleXIcon,
|
||||
HelpCircleIcon,
|
||||
TimerIcon,
|
||||
type LucideIcon
|
||||
} from 'lucide-react'
|
||||
import { HelpCircleIcon } from 'lucide-react'
|
||||
|
||||
import { Avatar, AvatarFallback } from '@repo/ui/components/ui/avatar'
|
||||
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 { 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.
|
||||
// You can use a Zod schema here if you want.
|
||||
type Course = {
|
||||
@@ -41,48 +36,6 @@ const formatted = new Intl.DateTimeFormat('pt-BR', {
|
||||
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>[] = [
|
||||
{
|
||||
id: 'select',
|
||||
@@ -105,10 +58,11 @@ export const columns: ColumnDef<Enrollment>[] = [
|
||||
)
|
||||
},
|
||||
{
|
||||
accessorKey: 'user',
|
||||
header: 'Colaborador',
|
||||
enableHiding: false,
|
||||
cell: ({ row: { original } }) => {
|
||||
const { user } = original
|
||||
cell: ({ row }) => {
|
||||
const user = row.getValue('user') as { name: string; email: string }
|
||||
|
||||
return (
|
||||
<div className="flex gap-2.5 items-center">
|
||||
@@ -127,22 +81,27 @@ export const columns: ColumnDef<Enrollment>[] = [
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'course.name',
|
||||
accessorKey: 'course',
|
||||
header: 'Curso',
|
||||
enableHiding: false,
|
||||
cell: ({ row: { original } }) => (
|
||||
<abbr className="truncate max-w-62 block" title={original.course.name}>
|
||||
{original.course.name}
|
||||
</abbr>
|
||||
)
|
||||
cell: ({ row }) => {
|
||||
const { name } = row.getValue('course') as { name: string }
|
||||
|
||||
return (
|
||||
<abbr className="truncate max-w-62 block" title={name}>
|
||||
{name}
|
||||
</abbr>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: 'Status',
|
||||
enableHiding: false,
|
||||
cell: ({ row: { original } }) => {
|
||||
const status = statusTranslate[original.status] ?? original.status
|
||||
const { icon: Icon, color } = statuses?.[original.status] ?? defaultIcon
|
||||
cell: ({ row }) => {
|
||||
const s = row.getValue('status') as string
|
||||
const status = labels[s] ?? s
|
||||
const { icon: Icon, color } = statuses?.[s] ?? { icon: HelpCircleIcon }
|
||||
|
||||
return (
|
||||
<Badge variant="outline" className={cn(color, ' px-1.5')}>
|
||||
@@ -156,44 +115,63 @@ export const columns: ColumnDef<Enrollment>[] = [
|
||||
accessorKey: 'progress',
|
||||
header: 'Progresso',
|
||||
enableHiding: false,
|
||||
cell: ({ row: { original } }) => (
|
||||
<div className="flex gap-2.5 items-center ">
|
||||
<Progress value={Number(original.progress)} className="w-32" />
|
||||
<span className="text-xs">{original.progress}%</span>
|
||||
</div>
|
||||
)
|
||||
cell: ({ row }) => {
|
||||
const progress = row.getValue('progress')
|
||||
|
||||
return (
|
||||
<div className="flex gap-2.5 items-center ">
|
||||
<Progress value={Number(progress)} className="w-32" />
|
||||
<span className="text-xs">{String(progress)}%</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'created_at',
|
||||
header: 'Matriculado em',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Matriculado em" />
|
||||
),
|
||||
meta: { title: 'Matriculado em' },
|
||||
enableSorting: true,
|
||||
enableHiding: true,
|
||||
cell: cellDate
|
||||
},
|
||||
{
|
||||
accessorKey: 'started_at',
|
||||
header: 'Iniciado em',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Iniciado em" />
|
||||
),
|
||||
meta: { title: 'Iniciado em' },
|
||||
enableSorting: true,
|
||||
enableHiding: true,
|
||||
cell: cellDate
|
||||
},
|
||||
{
|
||||
accessorKey: 'completed_at',
|
||||
header: 'Aprovado em',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Aprovado em" />
|
||||
),
|
||||
meta: { title: 'Aprovado em' },
|
||||
enableSorting: true,
|
||||
enableHiding: true,
|
||||
cell: cellDate
|
||||
},
|
||||
{
|
||||
accessorKey: 'failed_at',
|
||||
header: 'Reprovado em',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Reprovado em" />
|
||||
),
|
||||
meta: { title: 'Reprovado em' },
|
||||
enableSorting: true,
|
||||
enableHiding: true,
|
||||
cell: cellDate
|
||||
},
|
||||
{
|
||||
accessorKey: 'canceled_at',
|
||||
header: 'Cancelado em',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="Cancelado em" />
|
||||
),
|
||||
meta: { title: 'Cancelado em' },
|
||||
enableSorting: true,
|
||||
enableHiding: true,
|
||||
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 { PlusCircleIcon, PlusIcon } from 'lucide-react'
|
||||
import { DateTime } from 'luxon'
|
||||
import { MeiliSearchFilterBuilder } from 'meilisearch-helper'
|
||||
import { Suspense, useState } from 'react'
|
||||
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 { createSearch } from '@/lib/meili'
|
||||
|
||||
@@ -23,23 +22,20 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} 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) {
|
||||
return [{ title: 'Matrículas' }]
|
||||
}
|
||||
|
||||
const dtOptions = {
|
||||
zone: 'America/Sao_Paulo'
|
||||
}
|
||||
|
||||
export async function loader({ params, context, request }: Route.LoaderArgs) {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const { orgid } = params
|
||||
const query = searchParams.get('q') || ''
|
||||
const field = searchParams.get('field') || ''
|
||||
const from = searchParams.get('from') || ''
|
||||
const to = searchParams.get('to') || ''
|
||||
const sort = searchParams.get('sort') || 'created_at:desc'
|
||||
const status = searchParams.getAll('status') || []
|
||||
const page = Number(searchParams.get('p')) + 1
|
||||
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)
|
||||
}
|
||||
|
||||
if (field && from && to) {
|
||||
builder = builder.where(field, 'between', [from, to])
|
||||
if (from && to) {
|
||||
const [field, from_] = from.split(':')
|
||||
builder = builder.where(field, 'between', [from_, to])
|
||||
}
|
||||
|
||||
return {
|
||||
data: createSearch({
|
||||
index: 'betaeducacao-prod-enrollments',
|
||||
sort: ['created_at:desc'],
|
||||
filter: builder.build(),
|
||||
sort: [sort],
|
||||
query,
|
||||
page,
|
||||
hitsPerPage,
|
||||
@@ -75,10 +72,8 @@ const formatted = new Intl.DateTimeFormat('en-CA', {
|
||||
|
||||
export default function Route({ loaderData: { data } }) {
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
const [from, to] = [searchParams.get('from'), searchParams.get('to')]
|
||||
const [rangeField, setRangeField] = useState<string>(
|
||||
searchParams.get('field') || 'created_at'
|
||||
)
|
||||
// const [rangeField, setRangeField] = useState<string>('created_at')
|
||||
const { rangeField, dateRange } = useRangeParams('created_at')
|
||||
|
||||
return (
|
||||
<Suspense fallback={<Skeleton />}>
|
||||
@@ -95,6 +90,7 @@ export default function Route({ loaderData: { data } }) {
|
||||
{({ hits, page, hitsPerPage, totalHits }) => {
|
||||
return (
|
||||
<DataTable
|
||||
sort={[{ id: 'created_at', desc: true }]}
|
||||
columns={columns}
|
||||
data={hits as Enrollment[]}
|
||||
pageIndex={page - 1}
|
||||
@@ -158,21 +154,13 @@ export default function Route({ loaderData: { data } }) {
|
||||
|
||||
<RangeCalendarFilter
|
||||
className="lg:flex-1"
|
||||
value={
|
||||
from && to
|
||||
? {
|
||||
from: DateTime.fromISO(from, dtOptions),
|
||||
to: DateTime.fromISO(to, dtOptions)
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
value={dateRange}
|
||||
onChange={(dateRange) => {
|
||||
setSearchParams((searchParams) => {
|
||||
if (dateRange) {
|
||||
searchParams.set('field', rangeField)
|
||||
searchParams.set(
|
||||
'from',
|
||||
formatted.format(dateRange.from)
|
||||
`${rangeField}:${formatted.format(dateRange.from)}`
|
||||
)
|
||||
searchParams.set(
|
||||
'to',
|
||||
@@ -181,7 +169,6 @@ export default function Route({ loaderData: { data } }) {
|
||||
} else {
|
||||
searchParams.delete('from')
|
||||
searchParams.delete('to')
|
||||
searchParams.delete('field')
|
||||
}
|
||||
|
||||
return searchParams
|
||||
@@ -191,11 +178,13 @@ export default function Route({ loaderData: { data } }) {
|
||||
<div className="p-2.5">
|
||||
<Select
|
||||
defaultValue={rangeField}
|
||||
onValueChange={(field) => {
|
||||
setRangeField(field)
|
||||
onValueChange={(value) => {
|
||||
setSearchParams((searchParams) => {
|
||||
if (searchParams.has('from')) {
|
||||
searchParams.set('field', field)
|
||||
searchParams.set(
|
||||
'from',
|
||||
`${value}:${dateRange?.from?.getTime()}`
|
||||
)
|
||||
}
|
||||
|
||||
return searchParams
|
||||
@@ -230,7 +219,7 @@ export default function Route({ loaderData: { data } }) {
|
||||
</div>
|
||||
|
||||
<div className="lg:ml-auto flex gap-2.5">
|
||||
<CustomizeColumns className="flex-1" />
|
||||
<DataTableViewOptions className="flex-1" />
|
||||
|
||||
<Button className="flex-1" asChild>
|
||||
<Link to="add">
|
||||
@@ -247,3 +236,22 @@ export default function Route({ loaderData: { data } }) {
|
||||
</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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user