This commit is contained in:
2025-12-10 14:48:42 -03:00
parent 64ca0c7e81
commit 12f558233b
3 changed files with 76 additions and 55 deletions

View File

@@ -0,0 +1,34 @@
import { z } from 'zod'
export 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()
})
export const formSchema = z.object({
enrollments: z.array(enrollment).min(1).max(100)
})
export type Schema = z.infer<typeof formSchema>
export const MAX_ITEMS = 100

View File

@@ -3,21 +3,21 @@ import type { Route } from './+types/route'
import { useToggle } from 'ahooks' import { useToggle } from 'ahooks'
import { import {
CalendarIcon, CalendarIcon,
SearchIcon,
CopyIcon, CopyIcon,
CopyPlusIcon, CopyPlusIcon,
Trash2Icon, Trash2Icon,
PlusIcon, PlusIcon,
XIcon, XIcon,
ChevronsUpDownIcon, ChevronsUpDownIcon,
CheckIcon CheckIcon,
BookIcon,
UserIcon
} 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, use, useMemo, 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 { zodResolver } from '@hookform/resolvers/zod'
import { import {
@@ -39,8 +39,7 @@ import {
import { import {
InputGroup, InputGroup,
InputGroupAddon, InputGroupAddon,
InputGroupInput, InputGroupInput
InputGroupText
} from '@repo/ui/components/ui/input-group' } from '@repo/ui/components/ui/input-group'
import { import {
Card, Card,
@@ -61,43 +60,11 @@ import {
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 { createSearch } from '@repo/util/meili' import { createSearch } from '@repo/util/meili'
import { Await } from 'react-router'
import { cn } from '@repo/ui/lib/utils' import { cn } from '@repo/ui/lib/utils'
import Fuse from 'fuse.js' import Fuse from 'fuse.js'
import { useIsMobile } from '@repo/ui/hooks/use-mobile' import { useIsMobile } from '@repo/ui/hooks/use-mobile'
const enrollment = z.object({ import { formSchema, type Schema, MAX_ITEMS } from './data'
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' }]
@@ -125,7 +92,9 @@ export default function Route({
const form = useForm({ const form = useForm({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
defaultValues: { defaultValues: {
enrollments: [{ user: undefined, course: undefined, scheduled_for: null }] enrollments: [
{ user: undefined, course: undefined, scheduled_for: undefined }
]
} }
}) })
const { formState, control, handleSubmit, getValues } = form const { formState, control, handleSubmit, getValues } = form
@@ -203,9 +172,9 @@ export default function Route({
{index >= 1 && <div className="h-2.5 lg:hidden"></div>} {index >= 1 && <div className="h-2.5 lg:hidden"></div>}
<InputGroup> <InputGroup>
<InputGroupInput placeholder="Search..." /> <InputGroupInput placeholder="Colaborador" />
<InputGroupAddon> <InputGroupAddon>
<SearchIcon /> <UserIcon />
</InputGroupAddon> </InputGroupAddon>
</InputGroup> </InputGroup>
@@ -266,9 +235,11 @@ export default function Route({
type="button" type="button"
onClick={() => onClick={() =>
append({ append({
// @ts-ignore
user: undefined, user: undefined,
// @ts-ignore
course: undefined, course: undefined,
scheduled_for: null scheduled_for: undefined
}) })
} }
className="cursor-pointer" className="cursor-pointer"
@@ -333,7 +304,14 @@ function FacetedFilter({ value, onChange, options }: FacetedFilterProps) {
<Popover open={open} onOpenChange={set}> <Popover open={open} onOpenChange={set}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<InputGroup> <InputGroup>
<InputGroupInput readOnly value={value?.name || ''} /> <InputGroupInput
readOnly
placeholder="Curso"
value={value?.name || ''}
/>
<InputGroupAddon>
<BookIcon />
</InputGroupAddon>
<InputGroupAddon align="inline-end"> <InputGroupAddon align="inline-end">
<ChevronsUpDownIcon /> <ChevronsUpDownIcon />
</InputGroupAddon> </InputGroupAddon>
@@ -393,21 +371,28 @@ function FacetedFilter({ value, onChange, options }: FacetedFilterProps) {
) )
} }
function ScheduledForInput({ value, onChange }) { interface ScheduledForInputProps {
value?: Date
onChange?: (value: any) => void
}
function ScheduledForInput({ value, onChange }: ScheduledForInputProps) {
const today = new Date() const today = new Date()
const tomorrow = new Date() const tomorrow = new Date()
tomorrow.setDate(today.getDate() + 1) tomorrow.setDate(today.getDate() + 1)
const [open, { 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 ? format(selected, 'dd/MM/yyyy') : ''
? 'Imediatamente'
: format(selected, 'dd/MM/yyyy')
return ( return (
<Popover open={open} onOpenChange={set}> <Popover open={open} onOpenChange={set}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<InputGroup> <InputGroup>
<InputGroupInput readOnly type="search" value={displayValue} /> <InputGroupInput
readOnly
placeholder="Imediatamente"
value={displayValue}
/>
<InputGroupAddon> <InputGroupAddon>
<CalendarIcon /> <CalendarIcon />
</InputGroupAddon> </InputGroupAddon>

View File

@@ -87,7 +87,7 @@ export default function Component({
loaderData: { data } loaderData: { data }
}: Route.ComponentProps) { }: Route.ComponentProps) {
const [searchParams, setSearchParams] = useSearchParams() const [searchParams, setSearchParams] = useSearchParams()
const s = searchParams.get('s') as string const search = searchParams.get('s') as string
return ( return (
<Container className="space-y-4"> <Container className="space-y-4">
@@ -145,14 +145,16 @@ export default function Component({
</div> </div>
<Await resolve={data}> <Await resolve={data}>
{({ hits = [] }) => <List s={s} hits={hits as Enrollment[]} />} {({ hits = [] }) => (
<List search={search} hits={hits as Enrollment[]} />
)}
</Await> </Await>
</Suspense> </Suspense>
</Container> </Container>
) )
} }
function List({ s, hits = [] }: { s: string; hits: Enrollment[] }) { function List({ search, hits = [] }: { search: string; hits: Enrollment[] }) {
const fuse = useMemo(() => { const fuse = useMemo(() => {
return new Fuse(hits, { return new Fuse(hits, {
keys: ['course.name'], keys: ['course.name'],
@@ -162,12 +164,12 @@ function List({ s, hits = [] }: { s: string; hits: Enrollment[] }) {
}, [hits]) }, [hits])
const filtered = useMemo(() => { const filtered = useMemo(() => {
if (!s) { if (!search) {
return hits return hits
} }
return fuse.search(s).map(({ item }) => item) return fuse.search(search).map(({ item }) => item)
}, [s, fuse, hits]) }, [search, fuse, hits])
if (filtered.length === 0) { if (filtered.length === 0) {
return ( return (
@@ -176,7 +178,7 @@ function List({ s, hits = [] }: { s: string; hits: Enrollment[] }) {
<EmptyMedia variant="icon"> <EmptyMedia variant="icon">
<BanIcon /> <BanIcon />
</EmptyMedia> </EmptyMedia>
{s ? ( {search ? (
<> <>
<EmptyTitle>Nada encontrado</EmptyTitle> <EmptyTitle>Nada encontrado</EmptyTitle>
<EmptyDescription> <EmptyDescription>