add fuse
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
import type { Route } from './+types/route'
|
||||
|
||||
import Fuse from 'fuse.js'
|
||||
import { DateTime } from 'luxon'
|
||||
import { Suspense } from 'react'
|
||||
import { Suspense, useMemo } from 'react'
|
||||
import { BanIcon } from 'lucide-react'
|
||||
import { Await, useSearchParams } from 'react-router'
|
||||
|
||||
import { cn } from '@repo/ui/lib/utils'
|
||||
import { request as req } from '@repo/util/request'
|
||||
import { Skeleton } from '@repo/ui/components/skeleton'
|
||||
import { Await } from 'react-router'
|
||||
import { Card, CardContent } from '@repo/ui/components/ui/card'
|
||||
import {
|
||||
Table,
|
||||
@@ -19,7 +21,7 @@ import {
|
||||
} from '@repo/ui/components/ui/table'
|
||||
import { Abbr } from '@repo/ui/components/abbr'
|
||||
import { Button } from '@repo/ui/components/ui/button'
|
||||
import { cn } from '@repo/ui/lib/utils'
|
||||
import { SearchForm } from '@repo/ui/components/search-form'
|
||||
import {
|
||||
Empty,
|
||||
EmptyDescription,
|
||||
@@ -27,6 +29,7 @@ import {
|
||||
EmptyMedia,
|
||||
EmptyTitle
|
||||
} from '@repo/ui/components/ui/empty'
|
||||
import { Kbd } from '@repo/ui/components/ui/kbd'
|
||||
|
||||
import { billingPeriod, formatDate } from './util'
|
||||
import { RangePeriod } from './range-period'
|
||||
@@ -68,6 +71,9 @@ export async function loader({ context, request, params }: Route.LoaderArgs) {
|
||||
export default function Route({
|
||||
loaderData: { subscription, billing, startDate, endDate }
|
||||
}: Route.ComponentProps) {
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
const search = searchParams.get('s') as string
|
||||
|
||||
return (
|
||||
<>
|
||||
<Suspense fallback={<Skeleton />}>
|
||||
@@ -88,19 +94,36 @@ export default function Route({
|
||||
label: status,
|
||||
color
|
||||
} = statuses?.[billing?.status || 'CLOSED']
|
||||
const charges = items
|
||||
?.filter((item) => 'course' in item && item?.unit_price > 0)
|
||||
?.sort(sortBy('enrolled_at'))
|
||||
const credits = items
|
||||
?.filter((item) => 'course' in item && item?.unit_price < 0)
|
||||
?.sort(sortBy('created_at'))
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex max-lg:flex-col gap-2.5">
|
||||
<div className="w-full xl:w-1/4">
|
||||
<SearchForm
|
||||
defaultValue={search || ''}
|
||||
placeholder={
|
||||
<>
|
||||
Digite <Kbd>/</Kbd> para pesquisar
|
||||
</>
|
||||
}
|
||||
onChange={(value) =>
|
||||
setSearchParams((searchParams) => {
|
||||
searchParams.set('s', String(value))
|
||||
return searchParams
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<RangePeriod
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
billingDay={subscription?.billing_day || 1}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className={cn('pointer-events-none', color)}
|
||||
className={cn('pointer-events-none ml-auto', color)}
|
||||
variant="outline"
|
||||
asChild
|
||||
>
|
||||
@@ -108,146 +131,9 @@ export default function Route({
|
||||
<Icon className="size-3.5" /> {status}
|
||||
</span>
|
||||
</Button>
|
||||
|
||||
<RangePeriod
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
billingDay={subscription?.billing_day || 1}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{items.length ? (
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<Table className="table-auto w-full">
|
||||
{charges.length ? (
|
||||
<>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-muted-foreground/10 pointer-events-none">
|
||||
<TableHead>Colaborador</TableHead>
|
||||
<TableHead>Curso</TableHead>
|
||||
<TableHead>Matriculado por</TableHead>
|
||||
<TableHead>Matriculado em</TableHead>
|
||||
<TableHead>Valor unit.</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{charges?.map(
|
||||
(
|
||||
{
|
||||
user,
|
||||
course,
|
||||
author: created_by,
|
||||
unit_price,
|
||||
enrolled_at
|
||||
},
|
||||
index
|
||||
) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell>
|
||||
<Abbr>{user.name}</Abbr>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Abbr>{course.name}</Abbr>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Abbr>
|
||||
{created_by ? created_by.name : 'N/A'}
|
||||
</Abbr>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{datetime.format(new Date(enrolled_at))}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{currency.format(unit_price)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
)}
|
||||
</TableBody>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{credits.length ? (
|
||||
<>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-muted-foreground/10 pointer-events-none border-t">
|
||||
<TableHead colSpan={2}></TableHead>
|
||||
<TableHead>Cancelado por</TableHead>
|
||||
<TableHead colSpan={2}>Cancelado em</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{credits?.map(
|
||||
(
|
||||
{
|
||||
user,
|
||||
course,
|
||||
author: canceled_by,
|
||||
unit_price,
|
||||
created_at
|
||||
},
|
||||
index
|
||||
) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell>
|
||||
<Abbr>{user.name}</Abbr>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Abbr>{course.name}</Abbr>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Abbr>
|
||||
{canceled_by ? canceled_by.name : 'N/A'}
|
||||
</Abbr>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{datetime.format(new Date(created_at))}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{currency.format(unit_price)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
)}
|
||||
</TableBody>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<TableFooter>
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={4}
|
||||
className="text-right pointer-events-none"
|
||||
>
|
||||
Total
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{currency.format(
|
||||
items
|
||||
?.filter((x) => 'course' in x)
|
||||
.reduce(
|
||||
(acc, { unit_price }) => acc + unit_price,
|
||||
0
|
||||
)
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
</Table>
|
||||
</div>
|
||||
) : (
|
||||
<Empty className="border border-dashed">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<BanIcon />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>Nenhuma cobrança encontrada</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Não há nenhuma cobrança para este período.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
</Empty>
|
||||
)}
|
||||
<List items={items} search={search} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
@@ -257,6 +143,159 @@ export default function Route({
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function List({ items, search }) {
|
||||
const fuse = useMemo(() => {
|
||||
return new Fuse(items, {
|
||||
keys: ['user.name'],
|
||||
threshold: 0.3,
|
||||
includeMatches: true
|
||||
})
|
||||
}, [items])
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
if (!search) {
|
||||
return items
|
||||
}
|
||||
|
||||
return fuse.search(search).map(({ item }) => item)
|
||||
}, [search, fuse, items])
|
||||
|
||||
const charges = filtered
|
||||
?.filter((item) => 'course' in item && item?.unit_price > 0)
|
||||
?.sort(sortBy('enrolled_at'))
|
||||
const credits = filtered
|
||||
?.filter((item) => 'course' in item && item?.unit_price < 0)
|
||||
?.sort(sortBy('created_at'))
|
||||
|
||||
if (items.length === 0) {
|
||||
return (
|
||||
<Empty className="border border-dashed">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<BanIcon />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>Nenhuma cobrança encontrada</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Não há nenhuma cobrança para este período.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
</Empty>
|
||||
)
|
||||
}
|
||||
|
||||
if (filtered.length === 0) {
|
||||
return (
|
||||
<Empty className="border border-dashed">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<BanIcon />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>Nada encontrado</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Nenhum resultado para <mark>{search}</mark>.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
</Empty>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<Table className="table-auto w-full">
|
||||
{charges.length ? (
|
||||
<>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-muted-foreground/10 pointer-events-none">
|
||||
<TableHead>Colaborador</TableHead>
|
||||
<TableHead>Curso</TableHead>
|
||||
<TableHead>Matriculado por</TableHead>
|
||||
<TableHead>Matriculado em</TableHead>
|
||||
<TableHead>Valor unit.</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{charges?.map(
|
||||
(
|
||||
{ user, course, author: created_by, unit_price, enrolled_at },
|
||||
index
|
||||
) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell>
|
||||
<Abbr>{user.name}</Abbr>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Abbr>{course.name}</Abbr>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Abbr>{created_by ? created_by.name : 'N/A'}</Abbr>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{datetime.format(new Date(enrolled_at))}
|
||||
</TableCell>
|
||||
<TableCell>{currency.format(unit_price)}</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
)}
|
||||
</TableBody>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{credits.length ? (
|
||||
<>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-muted-foreground/10 pointer-events-none border-t">
|
||||
<TableHead colSpan={2}></TableHead>
|
||||
<TableHead>Cancelado por</TableHead>
|
||||
<TableHead colSpan={2}>Cancelado em</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{credits?.map(
|
||||
(
|
||||
{ user, course, author: canceled_by, unit_price, created_at },
|
||||
index
|
||||
) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell>
|
||||
<Abbr>{user.name}</Abbr>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Abbr>{course.name}</Abbr>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Abbr>{canceled_by ? canceled_by.name : 'N/A'}</Abbr>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{datetime.format(new Date(created_at))}
|
||||
</TableCell>
|
||||
<TableCell>{currency.format(unit_price)}</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
)}
|
||||
</TableBody>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<TableFooter>
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} className="text-right pointer-events-none">
|
||||
Total
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{currency.format(
|
||||
filtered
|
||||
?.filter((x) => 'course' in x)
|
||||
.reduce((acc, { unit_price }) => acc + unit_price, 0)
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
</Table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const currency = new Intl.NumberFormat('pt-BR', {
|
||||
style: 'currency',
|
||||
currency: 'BRL'
|
||||
|
||||
@@ -181,7 +181,7 @@ function List({ search, hits = [] }: { search: string; hits: Enrollment[] }) {
|
||||
<>
|
||||
<EmptyTitle>Nada encontrado</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Nenhum resultado para <mark>{s}</mark>.
|
||||
Nenhum resultado para <mark>{search}</mark>.
|
||||
</EmptyDescription>
|
||||
</>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user