This commit is contained in:
2025-12-23 13:42:50 -03:00
parent 22a2046fb1
commit 7ab0eab6bb
4 changed files with 150 additions and 117 deletions

View File

@@ -118,6 +118,7 @@ export function Assigned({ courses }: AssignedProps) {
onChange={onChange} onChange={onChange}
options={courses} options={courses}
error={fieldState.error} error={fieldState.error}
readOnly
/> />
<ErrorMessage <ErrorMessage
@@ -142,6 +143,7 @@ export function Assigned({ courses }: AssignedProps) {
<InputGroup> <InputGroup>
<InputGroupInput <InputGroupInput
className="pointer-events-none" className="pointer-events-none"
tabIndex={-1}
readOnly readOnly
value={currency.format(unit_price)} value={currency.format(unit_price)}
/> />

View File

@@ -20,8 +20,7 @@ import { CoursePicker } from '../_.$orgid.enrollments.add/course-picker'
import { MAX_ITEMS, type Course } from '../_.$orgid.enrollments.add/data' import { MAX_ITEMS, type Course } from '../_.$orgid.enrollments.add/data'
const emptyRow = { const emptyRow = {
course: undefined, course: undefined
quantity: undefined
} }
type BulkProps = { type BulkProps = {
@@ -54,8 +53,15 @@ export function Bulk({ courses }: BulkProps) {
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
defaultValues: { items: [emptyRow] } defaultValues: { items: [emptyRow] }
}) })
const { formState, control, handleSubmit, register, setValue, getValues } = const {
form formState,
control,
handleSubmit,
register,
setValue,
getValues,
setFocus
} = form
const { fields, remove, append } = useFieldArray({ const { fields, remove, append } = useFieldArray({
control, control,
name: 'items' name: 'items'
@@ -96,15 +102,19 @@ export function Bulk({ courses }: BulkProps) {
control={control} control={control}
name={`items.${index}.course`} name={`items.${index}.course`}
render={({ render={({
field: { name, value, onChange }, field: { name, value, onChange, ref },
fieldState fieldState
}) => ( }) => (
<div className="grid gap-1"> <div className="grid gap-1">
<CoursePicker <CoursePicker
ref={ref}
name={name}
autoFocus={index === 0}
value={value} value={value}
onChange={onChange} onChange={onChange}
options={courses} options={courses}
error={fieldState.error} error={fieldState.error}
readOnly
/> />
<ErrorMessage <ErrorMessage
@@ -139,6 +149,7 @@ export function Bulk({ courses }: BulkProps) {
<InputGroupAddon align="inline-end"> <InputGroupAddon align="inline-end">
<InputGroupButton <InputGroupButton
type="button" type="button"
tabIndex={-1}
size="icon-xs" size="icon-xs"
className="border cursor-pointer" className="border cursor-pointer"
onClick={() => { onClick={() => {
@@ -157,6 +168,7 @@ export function Bulk({ courses }: BulkProps) {
<InputGroupAddon align="inline-end"> <InputGroupAddon align="inline-end">
<InputGroupButton <InputGroupButton
type="button" type="button"
tabIndex={-1}
size="icon-xs" size="icon-xs"
className="border cursor-pointer" className="border cursor-pointer"
onClick={() => { onClick={() => {
@@ -172,6 +184,7 @@ export function Bulk({ courses }: BulkProps) {
<InputGroup> <InputGroup>
<InputGroupInput <InputGroupInput
tabIndex={-1}
className="pointer-events-none" className="pointer-events-none"
readOnly readOnly
value={currency.format(course?.unit_price || 0)} value={currency.format(course?.unit_price || 0)}
@@ -180,6 +193,7 @@ export function Bulk({ courses }: BulkProps) {
<InputGroup> <InputGroup>
<InputGroupInput <InputGroupInput
tabIndex={-1}
className="pointer-events-none" className="pointer-events-none"
readOnly readOnly
value={currency.format( value={currency.format(
@@ -207,8 +221,13 @@ export function Bulk({ courses }: BulkProps) {
<Button <Button
type="button" type="button"
// @ts-ignore onClick={() => {
onClick={() => append(emptyRow)} // @ts-ignore
append(emptyRow, { shouldFocus: false })
queueMicrotask(() => {
setFocus(`items.${fields.length}.course`)
})
}}
className="cursor-pointer" className="cursor-pointer"
disabled={fields.length == MAX_ITEMS} disabled={fields.length == MAX_ITEMS}
variant="outline" variant="outline"

View File

@@ -1,4 +1,10 @@
import { use, useState, useMemo } from 'react' import {
use,
useState,
useMemo,
forwardRef,
type InputHTMLAttributes
} from 'react'
import { useToggle } from 'ahooks' import { useToggle } from 'ahooks'
import Fuse from 'fuse.js' import Fuse from 'fuse.js'
import { import {
@@ -32,124 +38,129 @@ import {
import { type Course } from './data' import { type Course } from './data'
interface CoursePickerProps { interface CoursePickerProps extends Omit<
InputHTMLAttributes<HTMLInputElement>,
'value' | 'onChange'
> {
value?: Course value?: Course
options: Promise<{ hits: any[] }> options: Promise<{ hits: any[] }>
onChange?: (value: any) => void onChange?: (value: any) => void
error?: any error?: any
} }
export function CoursePicker({ export const CoursePicker = forwardRef<HTMLInputElement, CoursePickerProps>(
value, ({ value, onChange, options, error, ...props }, ref) => {
onChange, const { hits } = use(options)
options, const [search, setSearch] = useState<string>('')
error const [open, { set }] = useToggle()
}: CoursePickerProps) { const [sort, { toggle }] = useToggle('a-z', 'z-a')
const { hits } = use(options) const fuse = useMemo(() => {
const [search, setSearch] = useState<string>('') return new Fuse(hits, {
const [open, { set }] = useToggle() keys: ['name'],
const [sort, { toggle }] = useToggle('a-z', 'z-a') threshold: 0.3,
const fuse = useMemo(() => { includeMatches: true
return new Fuse(hits, { })
keys: ['name'], }, [hits])
threshold: 0.3,
includeMatches: true
})
}, [hits])
const filtered = useMemo(() => { const filtered = useMemo(() => {
const results = !search ? hits : fuse.search(search).map(({ item }) => item) const results = !search
? hits
: fuse.search(search).map(({ item }) => item)
return results.sort((a, b) => { return results.sort((a, b) => {
const comparison = a.name.localeCompare(b.name) const comparison = a.name.localeCompare(b.name)
return sort === 'a-z' ? comparison : -comparison return sort === 'a-z' ? comparison : -comparison
}) })
}, [search, fuse, hits, sort]) }, [search, fuse, hits, sort])
return ( return (
<Popover open={open} onOpenChange={set}> <Popover open={open} onOpenChange={set}>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<InputGroup> <InputGroup>
<InputGroupInput <InputGroupInput
readOnly ref={ref}
placeholder="Curso" placeholder="Curso"
value={value?.name || ''} value={value?.name || ''}
aria-invalid={!!error} aria-invalid={!!error}
/> {...props}
<InputGroupAddon> />
<BookIcon />
</InputGroupAddon>
<InputGroupAddon align="inline-end">
<ChevronsUpDownIcon />
</InputGroupAddon>
</InputGroup>
</PopoverTrigger>
<PopoverContent className="lg:w-84 p-0" align="start"> <InputGroupAddon>
<Command shouldFilter={false}> <BookIcon />
<div className="flex"> </InputGroupAddon>
<div className="flex-1">
<CommandInput <InputGroupAddon align="inline-end">
placeholder="Curso" <ChevronsUpDownIcon />
autoComplete="off" </InputGroupAddon>
onValueChange={setSearch} </InputGroup>
/> </PopoverTrigger>
<PopoverContent className="lg:w-84 p-0" align="start">
<Command shouldFilter={false}>
<div className="flex">
<div className="flex-1">
<CommandInput
placeholder="Curso"
autoComplete="off"
onValueChange={setSearch}
/>
</div>
<div className="border-b flex items-center justify-end">
<Button
variant="link"
size="icon-sm"
tabIndex={-1}
className="cursor-pointer text-muted-foreground"
onClick={toggle}
>
{sort == 'a-z' ? <ArrowDownAZIcon /> : <ArrowUpAZIcon />}
</Button>
</div>
</div> </div>
<div className="border-b flex items-center justify-end"> {/* Force rerender to reset the scroll position */}
<Button <CommandList key={sort}>
variant="link" <CommandEmpty>Nenhum resultado encontrado.</CommandEmpty>
size="icon-sm" <CommandGroup>
tabIndex={-1} {filtered
className="cursor-pointer text-muted-foreground" .filter(
onClick={toggle} ({ metadata__unit_price = 0 }) => metadata__unit_price > 0
>
{sort == 'a-z' ? <ArrowDownAZIcon /> : <ArrowUpAZIcon />}
</Button>
</div>
</div>
{/* Force rerender to reset the scroll position */}
<CommandList key={sort}>
<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>
) )
)} .map(
</CommandGroup> ({
</CommandList> id,
</Command> name,
</PopoverContent> access_period,
</Popover> 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>
)
}
)

View File

@@ -263,6 +263,7 @@ export default function Route({
onChange={onChange} onChange={onChange}
options={courses} options={courses}
error={fieldState.error} error={fieldState.error}
readOnly
/> />
<ErrorMessage <ErrorMessage