update filter
This commit is contained in:
@@ -7,14 +7,27 @@ import {
|
|||||||
CopyIcon,
|
CopyIcon,
|
||||||
CopyPlusIcon,
|
CopyPlusIcon,
|
||||||
Trash2Icon,
|
Trash2Icon,
|
||||||
PlusIcon
|
PlusIcon,
|
||||||
|
XIcon,
|
||||||
|
ChevronsUpDownIcon,
|
||||||
|
CheckIcon
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { Link } from 'react-router'
|
import { Link } from 'react-router'
|
||||||
import { Controller, useFieldArray, useForm } from 'react-hook-form'
|
import { Controller, useFieldArray, useForm } from 'react-hook-form'
|
||||||
import { Fragment, useState } from 'react'
|
import { Fragment, use, useMemo, useState } from 'react'
|
||||||
import { format } from 'date-fns'
|
import { format } from 'date-fns'
|
||||||
import { ptBR } from 'react-day-picker/locale'
|
import { ptBR } from 'react-day-picker/locale'
|
||||||
|
import { z } from 'zod'
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
|
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandList
|
||||||
|
} from '@repo/ui/components/ui/command'
|
||||||
import {
|
import {
|
||||||
Breadcrumb,
|
Breadcrumb,
|
||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
@@ -47,25 +60,88 @@ import {
|
|||||||
} from '@repo/ui/components/ui/popover'
|
} from '@repo/ui/components/ui/popover'
|
||||||
import { Label } from '@repo/ui/components/ui/label'
|
import { Label } from '@repo/ui/components/ui/label'
|
||||||
import { Calendar } from '@repo/ui/components/ui/calendar'
|
import { Calendar } from '@repo/ui/components/ui/calendar'
|
||||||
import { data } from 'react-router'
|
import { createSearch } from '@repo/util/meili'
|
||||||
|
import { Await } from 'react-router'
|
||||||
|
import { cn } from '@repo/ui/lib/utils'
|
||||||
|
import Fuse from 'fuse.js'
|
||||||
|
|
||||||
|
const enrollment = z.object({
|
||||||
|
user: z
|
||||||
|
.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
email: z.string(),
|
||||||
|
cpf: z.string()
|
||||||
|
})
|
||||||
|
.required(),
|
||||||
|
course: z
|
||||||
|
.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
access_period: z.number(),
|
||||||
|
unit_price: z.number()
|
||||||
|
})
|
||||||
|
.required(),
|
||||||
|
deduplication_window: z
|
||||||
|
.object({
|
||||||
|
offset_days: z.number()
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
scheduled_for: z.date().optional().nullable()
|
||||||
|
})
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
enrollments: z.array(enrollment).min(1).max(100)
|
||||||
|
})
|
||||||
|
|
||||||
|
type Schema = z.infer<typeof formSchema>
|
||||||
|
|
||||||
|
const MAX_ITEMS = 100
|
||||||
|
|
||||||
export function meta({}: Route.MetaArgs) {
|
export function meta({}: Route.MetaArgs) {
|
||||||
return [{ title: 'Adicionar matrícula' }]
|
return [{ title: 'Adicionar matrícula' }]
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Route({}: Route.ComponentProps) {
|
export async function loader({ context }: Route.LoaderArgs) {
|
||||||
const form = useForm({ defaultValues: { enrollments: [{}] } })
|
const courses = createSearch({
|
||||||
|
index: 'saladeaula_courses',
|
||||||
|
sort: ['created_at:desc'],
|
||||||
|
filter: 'unlisted NOT EXISTS',
|
||||||
|
hitsPerPage: 100,
|
||||||
|
env: context.cloudflare.env
|
||||||
|
})
|
||||||
|
|
||||||
|
return { courses }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function action({}: Route.ActionArgs) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Route({
|
||||||
|
loaderData: { courses }
|
||||||
|
}: Route.ComponentProps) {
|
||||||
|
const form = useForm({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
enrollments: [{ user: undefined, course: undefined, scheduled_for: null }]
|
||||||
|
}
|
||||||
|
})
|
||||||
const { formState, control, handleSubmit, getValues } = form
|
const { formState, control, handleSubmit, getValues } = form
|
||||||
const { fields, insert, remove, append } = useFieldArray({
|
const { fields, insert, remove, append } = useFieldArray({
|
||||||
control,
|
control,
|
||||||
name: 'enrollments'
|
name: 'enrollments'
|
||||||
})
|
})
|
||||||
|
|
||||||
const onSubmit = async (data) => {
|
const onSubmit = async (data: Schema) => {
|
||||||
console.log(data)
|
console.log(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
const duplicateRow = (index: number, times: number = 1) => {
|
const duplicateRow = (index: number, times: number = 1) => {
|
||||||
|
if (fields.length + times > MAX_ITEMS) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const values = getValues(`enrollments.${index}`)
|
const values = getValues(`enrollments.${index}`)
|
||||||
Array.from({ length: times }, (_, i) => {
|
Array.from({ length: times }, (_, i) => {
|
||||||
insert(index + 1 + i, values)
|
insert(index + 1 + i, values)
|
||||||
@@ -91,6 +167,9 @@ export default function Route({}: Route.ComponentProps) {
|
|||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
className="lg:max-w-4xl mx-auto space-y-2.5"
|
className="lg:max-w-4xl mx-auto space-y-2.5"
|
||||||
|
autoComplete="off"
|
||||||
|
data-1p-ignore="true"
|
||||||
|
data-lpignore="true"
|
||||||
>
|
>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -129,12 +208,17 @@ export default function Route({}: Route.ComponentProps) {
|
|||||||
</InputGroupAddon>
|
</InputGroupAddon>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
|
|
||||||
<InputGroup>
|
<Controller
|
||||||
<InputGroupInput placeholder="Search..." />
|
control={control}
|
||||||
<InputGroupAddon>
|
name={`enrollments.${index}.course`}
|
||||||
<SearchIcon />
|
render={({ field: { value, onChange } }) => (
|
||||||
</InputGroupAddon>
|
<FacetedFilter
|
||||||
</InputGroup>
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
options={courses}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
@@ -147,6 +231,7 @@ export default function Route({}: Route.ComponentProps) {
|
|||||||
{/* Action */}
|
{/* Action */}
|
||||||
<div className="flex gap-1.5">
|
<div className="flex gap-1.5">
|
||||||
<Button
|
<Button
|
||||||
|
type="button"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
@@ -178,8 +263,15 @@ export default function Route({}: Route.ComponentProps) {
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => append({})}
|
onClick={() =>
|
||||||
|
append({
|
||||||
|
user: undefined,
|
||||||
|
course: undefined,
|
||||||
|
scheduled_for: null
|
||||||
|
})
|
||||||
|
}
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
|
disabled={fields.length == MAX_ITEMS}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
@@ -203,22 +295,136 @@ export default function Route({}: Route.ComponentProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Course = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
access_period: number
|
||||||
|
metadata__unit_price?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FacetedFilterProps {
|
||||||
|
value?: Course
|
||||||
|
options: Promise<{ hits: any[] }>
|
||||||
|
onChange?: (value: any) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function FacetedFilter({ value, onChange, options }: FacetedFilterProps) {
|
||||||
|
const [search, setSearch] = useState('')
|
||||||
|
const [open, { set }] = useToggle()
|
||||||
|
const { hits } = use(options)
|
||||||
|
const fuse = useMemo(() => {
|
||||||
|
return new Fuse(hits, {
|
||||||
|
keys: ['name'],
|
||||||
|
threshold: 0.3,
|
||||||
|
includeMatches: true
|
||||||
|
})
|
||||||
|
}, [hits])
|
||||||
|
|
||||||
|
const filtered = useMemo(() => {
|
||||||
|
if (!search) {
|
||||||
|
return hits
|
||||||
|
}
|
||||||
|
|
||||||
|
return fuse.search(search).map(({ item }) => item)
|
||||||
|
}, [search, fuse, hits])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover open={open} onOpenChange={set}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<InputGroup>
|
||||||
|
<InputGroupInput readOnly value={value?.name || ''} />
|
||||||
|
<InputGroupAddon align="inline-end">
|
||||||
|
<ChevronsUpDownIcon />
|
||||||
|
</InputGroupAddon>
|
||||||
|
</InputGroup>
|
||||||
|
</PopoverTrigger>
|
||||||
|
|
||||||
|
<PopoverContent className="w-72 p-0" align="start">
|
||||||
|
<Command shouldFilter={false}>
|
||||||
|
<CommandInput
|
||||||
|
placeholder="Curso"
|
||||||
|
autoComplete="off"
|
||||||
|
onValueChange={setSearch}
|
||||||
|
/>
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty>Nenhum resultado encontrado.</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{filtered
|
||||||
|
.filter(
|
||||||
|
({ metadata__unit_price = 0 }) => metadata__unit_price > 0
|
||||||
|
)
|
||||||
|
.map(
|
||||||
|
({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
access_period,
|
||||||
|
metadata__unit_price: unit_price
|
||||||
|
}) => (
|
||||||
|
<CommandItem
|
||||||
|
key={id}
|
||||||
|
value={id}
|
||||||
|
className="cursor-pointer"
|
||||||
|
onSelect={() => {
|
||||||
|
onChange?.({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
access_period: Number(access_period),
|
||||||
|
unit_price: Number(unit_price)
|
||||||
|
})
|
||||||
|
set(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
<CheckIcon
|
||||||
|
className={cn(
|
||||||
|
'ml-auto',
|
||||||
|
value?.id === id ? 'opacity-100' : 'opacity-0'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</CommandItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function ScheduledForInput({ value, onChange }) {
|
function ScheduledForInput({ value, onChange }) {
|
||||||
const today = new Date()
|
const today = new Date()
|
||||||
const [open, { toggle, set }] = useToggle()
|
const [open, { set }] = useToggle()
|
||||||
const [selected, setDate] = useState<Date | undefined>(value)
|
const [selected, setDate] = useState<Date | undefined>(value)
|
||||||
const displayValue = !selected
|
const displayValue = !selected
|
||||||
? 'Imediatamente'
|
? 'Imediatamente'
|
||||||
: format(selected, 'dd/MM/yyyy')
|
: format(selected, 'dd/MM/yyyy')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover open={open} onOpenChange={toggle}>
|
<Popover open={open} onOpenChange={set}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<InputGroupInput readOnly value={displayValue} />
|
<InputGroupInput readOnly type="search" value={displayValue} />
|
||||||
<InputGroupAddon>
|
<InputGroupAddon>
|
||||||
<CalendarIcon />
|
<CalendarIcon />
|
||||||
</InputGroupAddon>
|
</InputGroupAddon>
|
||||||
|
{selected && (
|
||||||
|
<InputGroupAddon align="inline-end" className="mr-0">
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
size="icon-sm"
|
||||||
|
className="cursor-pointer text-black dark:text-white"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setDate(undefined)
|
||||||
|
onChange?.(undefined)
|
||||||
|
set(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<XIcon />
|
||||||
|
</Button>
|
||||||
|
</InputGroupAddon>
|
||||||
|
)}
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-full p-0" align="start">
|
<PopoverContent className="w-full p-0" align="start">
|
||||||
@@ -281,7 +487,6 @@ function DuplicateRowMultipleTimes({
|
|||||||
o preenchimento.
|
o preenchimento.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<div className="grid grid-cols-3 items-center gap-4">
|
<div className="grid grid-cols-3 items-center gap-4">
|
||||||
<Label htmlFor="quantity">Quantidade</Label>
|
<Label htmlFor="quantity">Quantidade</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -294,10 +499,20 @@ function DuplicateRowMultipleTimes({
|
|||||||
className="col-span-2"
|
className="col-span-2"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end gap-2.5">
|
||||||
<Button type="submit">Duplicar</Button>
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="link"
|
||||||
|
tabIndex={-1}
|
||||||
|
className="cursor-pointer dark:text-white text-black"
|
||||||
|
onClick={() => set(false)}
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" className="cursor-pointer">
|
||||||
|
Duplicar
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ function List({ s, hits = [] }: { s: string; hits: Enrollment[] }) {
|
|||||||
})
|
})
|
||||||
}, [hits])
|
}, [hits])
|
||||||
|
|
||||||
const hits_ = useMemo(() => {
|
const filtered = useMemo(() => {
|
||||||
if (!s) {
|
if (!s) {
|
||||||
return hits
|
return hits
|
||||||
}
|
}
|
||||||
@@ -169,7 +169,7 @@ function List({ s, hits = [] }: { s: string; hits: Enrollment[] }) {
|
|||||||
return fuse.search(s).map(({ item }) => item)
|
return fuse.search(s).map(({ item }) => item)
|
||||||
}, [s, fuse, hits])
|
}, [s, fuse, hits])
|
||||||
|
|
||||||
if (hits_.length === 0) {
|
if (filtered.length === 0) {
|
||||||
return (
|
return (
|
||||||
<Empty className="border border-dashed">
|
<Empty className="border border-dashed">
|
||||||
<EmptyHeader>
|
<EmptyHeader>
|
||||||
@@ -199,7 +199,7 @@ function List({ s, hits = [] }: { s: string; hits: Enrollment[] }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid lg:grid-cols-4 gap-5">
|
<div className="grid lg:grid-cols-4 gap-5">
|
||||||
{hits_.map((props: Enrollment, idx) => {
|
{filtered.map((props: Enrollment, idx) => {
|
||||||
return <Enrollment key={idx} {...props} />
|
return <Enrollment key={idx} {...props} />
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user