import type { Route } from './+types/route' import { useToggle } from 'ahooks' import { ErrorMessage } from '@hookform/error-message' import { CalendarIcon, CopyIcon, CopyPlusIcon, Trash2Icon, PlusIcon, XIcon, ChevronsUpDownIcon, CheckIcon, BookIcon, ArrowDownAZIcon, ArrowDownZAIcon, AlertTriangleIcon, UserIcon } from 'lucide-react' import { redirect, Link, useParams, useFetcher } from 'react-router' import { Controller, useFieldArray, useForm } from 'react-hook-form' import { Fragment, use, useMemo, useState } from 'react' import { format } from 'date-fns' import { ptBR } from 'react-day-picker/locale' import { zodResolver } from '@hookform/resolvers/zod' import Fuse from 'fuse.js' import { formatCPF } from '@brazilian-utils/brazilian-utils' import { Avatar, AvatarFallback } from '@repo/ui/components/ui/avatar' import { Abbr } from '@repo/ui/components/abbr' import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@repo/ui/components/ui/command' import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from '@repo/ui/components/ui/breadcrumb' import { InputGroup, InputGroupAddon, InputGroupInput } from '@repo/ui/components/ui/input-group' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@repo/ui/components/ui/card' import { Spinner } from '@repo/ui/components/ui/spinner' import { Input } from '@repo/ui/components/ui/input' import { Button } from '@repo/ui/components/ui/button' import { Separator } from '@repo/ui/components/ui/separator' import { Popover, PopoverContent, PopoverTrigger } from '@repo/ui/components/ui/popover' import { Label } from '@repo/ui/components/ui/label' import { Calendar } from '@repo/ui/components/ui/calendar' import { createSearch } from '@repo/util/meili' import { initials, cn } from '@repo/ui/lib/utils' import { HttpMethod, request as req } from '@repo/util/request' import { useIsMobile } from '@repo/ui/hooks/use-mobile' import { cloudflareContext } from '@repo/auth/context' import { SearchFilter } from '@repo/ui/components/search-filter' import { formSchema, type Schema, MAX_ITEMS } from './data' export function meta({}: Route.MetaArgs) { return [{ title: 'Adicionar matrícula' }] } export async function loader({ context }: Route.LoaderArgs) { const cloudflare = context.get(cloudflareContext) const courses = createSearch({ index: 'saladeaula_courses', sort: ['created_at:desc'], filter: 'unlisted NOT EXISTS', hitsPerPage: 100, env: cloudflare.env }) return { courses } } export async function action({ params, request, context }: Route.ActionArgs) { const body = (await request.json()) as object const r = await req({ url: `enrollments`, headers: new Headers({ 'Content-Type': 'application/json' }), method: HttpMethod.POST, body: JSON.stringify({ org_id: params.orgid, ...body }), request, context }) const data = (await r.json()) as { sk: string } return redirect(`/${params.orgid}/enrollments/${data.sk}/submitted`) } const emptyRow = { user: undefined, course: undefined, scheduled_for: undefined } export default function Route({ loaderData: { courses } }: Route.ComponentProps) { const { orgid } = useParams() const fetcher = useFetcher() const form = useForm({ resolver: zodResolver(formSchema), defaultValues: { enrollments: [emptyRow] } }) const { formState, control, handleSubmit, getValues, reset } = form const { fields, insert, remove, append } = useFieldArray({ control, name: 'enrollments' }) const onSubmit = async (data: Schema) => { await fetcher.submit(JSON.stringify(data), { method: 'post', encType: 'application/json' }) } const onSearch = async (search: string) => { const params = new URLSearchParams({ q: search }) const r = await fetch(`/${orgid}/users.json?${params.toString()}`) const { hits } = (await r.json()) as { hits: any[] } return hits } const duplicateRow = (index: number, times: number = 1) => { if (fields.length + times > MAX_ITEMS) { return null } const { user, ...rest } = getValues(`enrollments.${index}`) Array.from({ length: times }, (_, i) => { // @ts-ignore insert(index + 1 + i, rest) }) } return (
Matrículas Adicionar matrículas
Adicionar matrículas Siga os passos abaixo para adicionar novas matrículas.
{/* Header */} <>
Colaborador
Curso
Matriculado em
{/**/}
{/* Rows */} <> {fields.map((field, index) => ( {/* Separator only for mobile */} {index >= 1 &&
} (
( { onSelect() onClose() }} >
{initials(name)}
  • {name}
  • {email}
  • {cpf ? (
  • {formatCPF(cpf)}
  • ) : (
  • Inelegível
  • )}
)} > {({ loading }) => ( {value && ( )} {loading && ( )} )}
(

{message}

)} />
)} /> (
(

{message}

)} />
)} /> ( )} /> {/* Action */}
))}
) } 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 error?: any } function FacetedFilter({ value, onChange, options, error }: FacetedFilterProps) { const [search, setSearch] = useState('') const [open, { set }] = useToggle() const [sort, { toggle }] = useToggle('a-z', 'z-a') const { hits } = use(options) const fuse = useMemo(() => { return new Fuse(hits, { keys: ['name'], threshold: 0.3, includeMatches: true }) }, [hits]) const filtered = useMemo(() => { const results = !search ? hits : fuse.search(search).map(({ item }) => item) return results.sort((a, b) => { const comparison = a.name.localeCompare(b.name) return sort === 'a-z' ? comparison : -comparison }) }, [search, fuse, hits, sort]) return (
{/* Force rerender to reset the scroll position */} Nenhum resultado encontrado. {filtered .filter( ({ metadata__unit_price = 0 }) => metadata__unit_price > 0 ) .map( ({ id, name, access_period, metadata__unit_price: unit_price }) => ( { onChange?.({ id, name, access_period: Number(access_period), unit_price: Number(unit_price) }) set(false) }} > {name} ) )}
) } interface ScheduledForInputProps { value?: Date onChange?: (value: Date | undefined) => void } function ScheduledForInput({ value, onChange }: ScheduledForInputProps) { const today = new Date() const tomorrow = new Date() tomorrow.setDate(today.getDate() + 1) const [open, { set }] = useToggle() const [selected, setDate] = useState(value) const display = selected ? format(selected, 'dd/MM/yyyy') : '' return ( {selected && ( )} { setDate(date) onChange?.(date) set(false) }} disabled={{ before: tomorrow }} startMonth={new Date(today.getFullYear(), 0)} endMonth={new Date(today.getFullYear() + 3, 11)} captionLayout="dropdown" locale={ptBR} /> ) } function DuplicateRowMultipleTimes({ index, duplicateRow }: { index: number duplicateRow: (index: number, times: number) => void }) { const [open, { toggle, set }] = useToggle() const isMobile = useIsMobile() return (
{ e.stopPropagation() e.preventDefault() const formData = new FormData(e.currentTarget) const times = parseInt(formData.get('quantity') as string) duplicateRow(index, times) set(false) }} >

Duplicar várias vezes

Duplique o curso desta linha na quantidade desejada para agilizar o preenchimento.

) }