diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.add/async-combobox.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.add/async-combobox.tsx new file mode 100644 index 0000000..732173f --- /dev/null +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.add/async-combobox.tsx @@ -0,0 +1,128 @@ +import { useRequest, useToggle } from 'ahooks' +import { debounce } from 'lodash' +import { CheckIcon, UserIcon } from 'lucide-react' +import { initials, cn } from '@repo/ui/lib/utils' +import { formatCPF } from '@brazilian-utils/brazilian-utils' +import { Avatar, AvatarFallback } from '@repo/ui/components/ui/avatar' +import { Abbr } from '@repo/ui/components/abbr' +import { + InputGroup, + InputGroupAddon, + InputGroupInput +} from '@repo/ui/components/ui/input-group' + +import { + Popover, + PopoverContent, + PopoverTrigger +} from '@repo/ui/components/ui/popover' +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList +} from '@repo/ui/components/ui/command' +import { Spinner } from '@repo/ui/components/ui/spinner' + +interface AsyncComboboxProps { + value: any + title: string + onChange: (props: any) => void + onSearch: (search: string) => Promise +} +export function AsyncCombobox({ + title, + value, + onSearch, + onChange +}: AsyncComboboxProps) { + const [open, { set }] = useToggle() + const { + data = [], + loading, + runAsync + } = useRequest(onSearch, { manual: true, defaultParams: [''] }) + + return ( + + + + + + + + + {loading && ( + + + + )} + + + + + + + + + Nenhum resultado encontrado. + + {data.map(({ id, name, email, cpf }) => ( + { + onChange?.({ id, name, email, cpf }) + set(false) + }} + > +
+ + + {initials(name)} + + + +
    +
  • + {name} +
  • +
  • + {email} +
  • + {cpf && ( +
  • + {formatCPF(cpf)} +
  • + )} +
+
+ + +
+ ))} +
+
+
+
+
+ ) +} diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.add/route.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.add/route.tsx index 04d13d5..d5ed20a 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.add/route.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.add/route.tsx @@ -1,6 +1,6 @@ import type { Route } from './+types/route' -import { useToggle } from 'ahooks' +import { useRequest, useToggle } from 'ahooks' import { CalendarIcon, CopyIcon, @@ -10,15 +10,15 @@ import { XIcon, ChevronsUpDownIcon, CheckIcon, - BookIcon, - UserIcon + BookIcon } from 'lucide-react' -import { Link } from 'react-router' +import { Link, useParams } from 'react-router' import { Controller, useFieldArray, useForm } from 'react-hook-form' -import { Fragment, use, useMemo, useState } from 'react' +import { Fragment, use, useEffect, 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 { Command, @@ -61,10 +61,10 @@ import { Label } from '@repo/ui/components/ui/label' import { Calendar } from '@repo/ui/components/ui/calendar' import { createSearch } from '@repo/util/meili' import { cn } from '@repo/ui/lib/utils' -import Fuse from 'fuse.js' import { useIsMobile } from '@repo/ui/hooks/use-mobile' import { formSchema, type Schema, MAX_ITEMS } from './data' +import { AsyncCombobox } from './async-combobox' export function meta({}: Route.MetaArgs) { return [{ title: 'Adicionar matrícula' }] @@ -86,16 +86,20 @@ export async function action({}: Route.ActionArgs) { return {} } +const emptyRow = { + user: undefined, + course: undefined, + scheduled_for: undefined +} + export default function Route({ loaderData: { courses } }: Route.ComponentProps) { + const { orgid } = useParams() + const [data, setData] = useState({}) const form = useForm({ resolver: zodResolver(formSchema), - defaultValues: { - enrollments: [ - { user: undefined, course: undefined, scheduled_for: undefined } - ] - } + defaultValues: { enrollments: [emptyRow] } }) const { formState, control, handleSubmit, getValues } = form const { fields, insert, remove, append } = useFieldArray({ @@ -104,17 +108,25 @@ export default function Route({ }) const onSubmit = async (data: Schema) => { - console.log(data) + setData(data) + } + + 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 + return null } - const values = getValues(`enrollments.${index}`) + const { user, ...rest } = getValues(`enrollments.${index}`) Array.from({ length: times }, (_, i) => { - insert(index + 1 + i, values) + // @ts-ignore + insert(index + 1 + i, rest) }) } @@ -145,7 +157,7 @@ export default function Route({ Adicionar matrícula - Siga os passos abaixo para adicionar uma nova matrícula + Siga os passos abaixo para adicionar novas matrículas. @@ -162,21 +174,28 @@ export default function Route({
Matriculado em
-
+
{/**/}
{/* Rows */} <> {fields.map((field, index) => ( + {/* Separator only for mobile */} {index >= 1 &&
} - - - - - - + ( + + )} + /> - append({ - // @ts-ignore - user: undefined, - // @ts-ignore - course: undefined, - scheduled_for: undefined - }) - } + // @ts-ignore + onClick={() => append(emptyRow)} className="cursor-pointer" disabled={fields.length == MAX_ITEMS} variant="outline" @@ -258,11 +270,17 @@ export default function Route({ disabled={formState.isSubmitting} > {formState.isSubmitting && } - Continuar + Matricular + + {data && ( +
+          {JSON.stringify(data, null, 2)}
+        
+ )} ) } @@ -281,7 +299,7 @@ interface FacetedFilterProps { } function FacetedFilter({ value, onChange, options }: FacetedFilterProps) { - const [search, setSearch] = useState('') + const [search, setSearch] = useState('') const [open, { set }] = useToggle() const { hits } = use(options) const fuse = useMemo(() => { @@ -318,7 +336,7 @@ function FacetedFilter({ value, onChange, options }: FacetedFilterProps) { - + - +
{ e.stopPropagation() diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.users[.]json/route.ts b/apps/admin.saladeaula.digital/app/routes/_.$orgid.users[.]json/route.ts new file mode 100644 index 0000000..4fd3924 --- /dev/null +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.users[.]json/route.ts @@ -0,0 +1,30 @@ +import type { Route } from './+types/route' + +import { MeiliSearchFilterBuilder } from 'meilisearch-helper' +import { data } from 'react-router' + +import { createSearch } from '@repo/util/meili' + +export async function loader({ params, context, request }: Route.LoaderArgs) { + const { searchParams } = new URL(request.url) + const { orgid } = params + const query = searchParams.get('q') || '' + const sort = searchParams.get('sort') || 'createDate:desc' + const page = Number(searchParams.get('p')) + 1 + const hitsPerPage = Number(searchParams.get('perPage')) || 25 + + // Post-migration (users): rename `tenant_id` to `org_id` + let builder = new MeiliSearchFilterBuilder().where('tenant_id', '=', orgid) + + const r = await createSearch({ + index: 'betaeducacao-prod-users_d2o3r5gmm4it7j', + filter: builder.build(), + sort: [sort], + query, + page, + hitsPerPage, + env: context.cloudflare.env + }) + + return data(r) +} diff --git a/apps/insights.saladeaula.digital/app/routes/_app.orgs._index/route.tsx b/apps/insights.saladeaula.digital/app/routes/_app.orgs._index/route.tsx index 1032d80..4621db8 100644 --- a/apps/insights.saladeaula.digital/app/routes/_app.orgs._index/route.tsx +++ b/apps/insights.saladeaula.digital/app/routes/_app.orgs._index/route.tsx @@ -21,7 +21,7 @@ export async function loader({ context, request }: Route.LoaderArgs) { const page = Number(searchParams.get('p')) + 1 const hitsPerPage = Number(searchParams.get('perPage')) || 25 - const users = createSearch({ + const orgs = createSearch({ index: 'betaeducacao-prod-users_d2o3r5gmm4it7j', sort: ['createDate:desc', 'create_date:desc'], filter: 'cnpj EXISTS', @@ -31,7 +31,7 @@ export async function loader({ context, request }: Route.LoaderArgs) { env: context.cloudflare.env }) - return { data: users } + return { data: orgs } } export default function Route({ loaderData: { data } }: Route.ComponentProps) { diff --git a/apps/insights.saladeaula.digital/app/routes/_app.orgs[.]json/route.ts b/apps/insights.saladeaula.digital/app/routes/_app.orgs[.]json/route.ts new file mode 100644 index 0000000..e131675 --- /dev/null +++ b/apps/insights.saladeaula.digital/app/routes/_app.orgs[.]json/route.ts @@ -0,0 +1,23 @@ +import type { Route } from './+types/route' + +import { createSearch } from '@repo/util/meili' +import { data } from 'react-router' + +export async function loader({ context, request }: Route.LoaderArgs) { + const { searchParams } = new URL(request.url) + const query = searchParams.get('q') || '' + const page = Number(searchParams.get('p')) + 1 + const hitsPerPage = Number(searchParams.get('perPage')) || 25 + + const r = await createSearch({ + index: 'betaeducacao-prod-users_d2o3r5gmm4it7j', + sort: ['createDate:desc', 'create_date:desc'], + filter: 'cnpj EXISTS', + query, + page, + hitsPerPage, + env: context.cloudflare.env + }) + + return data(r) +} diff --git a/apps/saladeaula.digital/app/routes/index.tsx b/apps/saladeaula.digital/app/routes/index.tsx index dec0a63..9a18e57 100644 --- a/apps/saladeaula.digital/app/routes/index.tsx +++ b/apps/saladeaula.digital/app/routes/index.tsx @@ -104,7 +104,7 @@ export default function Component({
Digite / para pesquisar diff --git a/packages/ui/src/components/faceted-filter.tsx b/packages/ui/src/components/faceted-filter.tsx index 07ad3e1..78d59f2 100644 --- a/packages/ui/src/components/faceted-filter.tsx +++ b/packages/ui/src/components/faceted-filter.tsx @@ -78,6 +78,7 @@ export function FacetedFilter({ )} + @@ -120,6 +121,7 @@ export function FacetedFilter({ ) })} + {selectedValues.size > 0 && ( <>