update items

This commit is contained in:
2025-12-26 11:19:14 -03:00
parent e7aa6a6694
commit 3cdded360f
11 changed files with 392 additions and 26 deletions

View File

@@ -43,6 +43,8 @@ export const formSchema = z.object({
export type Schema = z.infer<typeof formSchema> export type Schema = z.infer<typeof formSchema>
export type Enrollment = z.infer<typeof enrollment>
export type User = { export type User = {
id: string id: string
name: string name: string

View File

@@ -213,7 +213,7 @@ export default function Route({
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="grid lg:grid-cols-[repeat(3,1fr)_auto] w-full gap-1.5"> <div className="grid lg:grid-cols-[repeat(3,1fr)_auto] w-full gap-3">
{/* Header */} {/* Header */}
<> <>
<Cell>Colaborador</Cell> <Cell>Colaborador</Cell>

View File

@@ -5,11 +5,11 @@ import { useToggle } from 'ahooks'
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 { Button } from '@repo/ui/components/ui/button'
import { Calendar } from '@repo/ui/components/ui/calendar' import { Calendar } from '@repo/ui/components/ui/calendar'
import { import {
InputGroup, InputGroup,
InputGroupAddon, InputGroupAddon,
InputGroupButton,
InputGroupInput InputGroupInput
} from '@repo/ui/components/ui/input-group' } from '@repo/ui/components/ui/input-group'
import { import {
@@ -46,11 +46,12 @@ export function ScheduledForInput({ value, onChange }: ScheduledForInputProps) {
</InputGroupAddon> </InputGroupAddon>
{selected && ( {selected && (
<InputGroupAddon align="inline-end" className="mr-0"> <InputGroupAddon align="inline-end">
<Button <InputGroupButton
variant="link" variant="ghost"
size="icon-sm" tabIndex={-1}
className="cursor-pointer text-muted-foreground" size="icon-xs"
className="cursor-pointer"
onClick={(e) => { onClick={(e) => {
e.preventDefault() e.preventDefault()
setDate(undefined) setDate(undefined)
@@ -59,7 +60,7 @@ export function ScheduledForInput({ value, onChange }: ScheduledForInputProps) {
}} }}
> >
<XIcon /> <XIcon />
</Button> </InputGroupButton>
</InputGroupAddon> </InputGroupAddon>
)} )}
</InputGroup> </InputGroup>

View File

@@ -3,13 +3,13 @@ import { XIcon, CheckIcon, AlertTriangleIcon, UserIcon } from 'lucide-react'
import { formatCPF } from '@brazilian-utils/brazilian-utils' import { formatCPF } from '@brazilian-utils/brazilian-utils'
import { cn, initials } from '@repo/ui/lib/utils' import { cn, initials } from '@repo/ui/lib/utils'
import { Button } from '@repo/ui/components/ui/button'
import { Avatar, AvatarFallback } from '@repo/ui/components/ui/avatar' import { Avatar, AvatarFallback } from '@repo/ui/components/ui/avatar'
import { Abbr } from '@repo/ui/components/abbr' import { Abbr } from '@repo/ui/components/abbr'
import { Spinner } from '@repo/ui/components/ui/spinner' import { Spinner } from '@repo/ui/components/ui/spinner'
import { import {
InputGroup, InputGroup,
InputGroupAddon, InputGroupAddon,
InputGroupButton,
InputGroupInput InputGroupInput
} from '@repo/ui/components/ui/input-group' } from '@repo/ui/components/ui/input-group'
import { CommandItem } from '@repo/ui/components/ui/command' import { CommandItem } from '@repo/ui/components/ui/command'
@@ -100,17 +100,18 @@ export function UserPicker({
{value && ( {value && (
<InputGroupAddon align="inline-end" className="mr-0"> <InputGroupAddon align="inline-end" className="mr-0">
<Button <InputGroupButton
variant="link" variant="ghost"
size="icon-sm" tabIndex={-1}
className="cursor-pointer text-muted-foreground" size="icon-xs"
className="cursor-pointer"
onClick={(e) => { onClick={(e) => {
e.preventDefault() e.preventDefault()
onChange?.(null) onChange?.(null)
}} }}
> >
<XIcon /> <XIcon />
</Button> </InputGroupButton>
</InputGroupAddon> </InputGroupAddon>
)} )}

View File

@@ -30,6 +30,7 @@ import {
import { import {
MAX_ITEMS, MAX_ITEMS,
formSchema, formSchema,
type Enrollment,
type Course, type Course,
type User type User
} from '../_.$orgid.enrollments.add/data' } from '../_.$orgid.enrollments.add/data'
@@ -38,7 +39,6 @@ import { Cell } from '../_.$orgid.enrollments.add/route'
import { CoursePicker } from '../_.$orgid.enrollments.add/course-picker' import { CoursePicker } from '../_.$orgid.enrollments.add/course-picker'
import { UserPicker } from '../_.$orgid.enrollments.add/user-picker' import { UserPicker } from '../_.$orgid.enrollments.add/user-picker'
import { Summary } from './bulk' import { Summary } from './bulk'
import { applyDiscount } from './discount'
import { currency } from './utils' import { currency } from './utils'
import { useWizard } from '@/components/wizard' import { useWizard } from '@/components/wizard'
@@ -48,6 +48,11 @@ const emptyRow = {
scheduled_for: undefined scheduled_for: undefined
} }
type Item = {
course: Enrollment['course']
quantity: number
}
const formSchemaAssigned = formSchema.extend({ const formSchemaAssigned = formSchema.extend({
coupon: z coupon: z
.object({ .object({
@@ -101,8 +106,6 @@ export function Assigned({
0 0
) )
console.log(coupon)
const onSearch = async (search: string) => { const onSearch = async (search: string) => {
const params = new URLSearchParams({ q: search }) const params = new URLSearchParams({ q: search })
const r = await fetch(`/${orgid}/users.json?${params.toString()}`) const r = await fetch(`/${orgid}/users.json?${params.toString()}`)
@@ -110,15 +113,25 @@ export function Assigned({
return hits return hits
} }
const onSubmit_ = async (data: Schema) => { const onSubmit_ = async ({ enrollments }: Schema) => {
await onSubmit(data) const items = Object.values(
enrollments.reduce<Record<string, Item>>((acc, e) => {
const id = e.course.id
acc[id] ??= { course: e.course, quantity: 0 }
acc[id].quantity++
return acc
}, {})
)
await onSubmit({ enrollments, items })
wizard('payment') wizard('payment')
} }
return ( return (
<Form {...form}> <Form {...form}>
<form onSubmit={handleSubmit(onSubmit_)} className="space-y-4"> <form onSubmit={handleSubmit(onSubmit_)} className="space-y-4">
<div className="grid w-full gap-1.5 lg:grid-cols-[3fr_3fr_2fr_2fr_auto]"> <div className="grid w-full gap-3 lg:grid-cols-[3fr_3fr_2fr_2fr_auto]">
{/* Header */} {/* Header */}
<> <>
<Cell>Colaborador</Cell> <Cell>Colaborador</Cell>

View File

@@ -125,7 +125,7 @@ export function Bulk({
return ( return (
<Form {...form}> <Form {...form}>
<form onSubmit={handleSubmit(onSubmit_)} className="space-y-4"> <form onSubmit={handleSubmit(onSubmit_)} className="space-y-4">
<div className="grid w-full gap-1.5 lg:grid-cols-[4fr_2fr_2fr_2fr_auto]"> <div className="grid w-full gap-3 lg:grid-cols-[4fr_2fr_2fr_2fr_auto]">
{/* Header */} {/* Header */}
<> <>
<Cell>Curso</Cell> <Cell>Curso</Cell>

View File

@@ -1,4 +1,4 @@
import { useForm, Controller } from 'react-hook-form' import { useForm, Controller, useWatch } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod' import { zodResolver } from '@hookform/resolvers/zod'
import { ErrorMessage } from '@hookform/error-message' import { ErrorMessage } from '@hookform/error-message'
import z from 'zod' import z from 'zod'
@@ -9,7 +9,28 @@ import { Label } from '@repo/ui/components/ui/label'
import { RadioGroup, RadioGroupItem } from '@repo/ui/components/ui/radio-group' import { RadioGroup, RadioGroupItem } from '@repo/ui/components/ui/radio-group'
import { Separator } from '@repo/ui/components/ui/separator' import { Separator } from '@repo/ui/components/ui/separator'
import { Checkbox } from '@repo/ui/components/ui/checkbox'
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet
} from '@repo/ui/components/ui/field'
import { Input } from '@repo/ui/components/ui/input'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from '@repo/ui/components/ui/select'
import { Textarea } from '@repo/ui/components/ui/textarea'
import { useWizard } from '@/components/wizard' import { useWizard } from '@/components/wizard'
import { Card, CardContent } from '@repo/ui/components/ui/card'
const formSchema = z.object({ const formSchema = z.object({
payment_method: z.enum(['PIX', 'BANK_SLIP', 'CREDIT_CARD'], { payment_method: z.enum(['PIX', 'BANK_SLIP', 'CREDIT_CARD'], {
@@ -33,6 +54,7 @@ export function Payment({ onSubmit, defaultValues }: PaymentProps) {
}, },
resolver: zodResolver(formSchema) resolver: zodResolver(formSchema)
}) })
const paymentMethod = useWatch({ control, name: 'payment_method' })
const onSubmit_ = async (data: Schema) => { const onSubmit_ = async (data: Schema) => {
await onSubmit(data) await onSubmit(data)
@@ -82,6 +104,8 @@ export function Payment({ onSubmit, defaultValues }: PaymentProps) {
)} )}
/> />
{paymentMethod === 'CREDIT_CARD' ? <CreditCard /> : null}
<Separator /> <Separator />
<div className="flex justify-between gap-4 *:cursor-pointer"> <div className="flex justify-between gap-4 *:cursor-pointer">
@@ -100,3 +124,78 @@ export function Payment({ onSubmit, defaultValues }: PaymentProps) {
</form> </form>
) )
} }
export function CreditCard() {
return (
<Card className="lg:max-w-md">
<CardContent>
<FieldGroup>
<FieldSet>
<FieldGroup>
<Field>
<FieldLabel htmlFor="checkout-7j9-card-number-uw1">
Número do cartão
</FieldLabel>
<Input id="checkout-7j9-card-number-uw1" required />
</Field>
<Field>
<FieldLabel htmlFor="checkout-7j9-card-name-43j">
Nome do titular
</FieldLabel>
<Input required />
</Field>
<div className="grid grid-cols-3 gap-4">
<Field>
<FieldLabel htmlFor="checkout-exp-month-ts6">Mês</FieldLabel>
<Select defaultValue="">
<SelectTrigger id="checkout-exp-month-ts6">
<SelectValue placeholder="MM" />
</SelectTrigger>
<SelectContent>
<SelectItem value="01">01</SelectItem>
<SelectItem value="02">02</SelectItem>
<SelectItem value="03">03</SelectItem>
<SelectItem value="04">04</SelectItem>
<SelectItem value="05">05</SelectItem>
<SelectItem value="06">06</SelectItem>
<SelectItem value="07">07</SelectItem>
<SelectItem value="08">08</SelectItem>
<SelectItem value="09">09</SelectItem>
<SelectItem value="10">10</SelectItem>
<SelectItem value="11">11</SelectItem>
<SelectItem value="12">12</SelectItem>
</SelectContent>
</Select>
</Field>
<Field>
<FieldLabel htmlFor="checkout-7j9-exp-year-f59">
Ano
</FieldLabel>
<Select defaultValue="">
<SelectTrigger id="checkout-7j9-exp-year-f59">
<SelectValue placeholder="AAAA" />
</SelectTrigger>
<SelectContent>
<SelectItem value="2024">2024</SelectItem>
<SelectItem value="2025">2025</SelectItem>
<SelectItem value="2026">2026</SelectItem>
<SelectItem value="2027">2027</SelectItem>
<SelectItem value="2028">2028</SelectItem>
<SelectItem value="2029">2029</SelectItem>
</SelectContent>
</Select>
</Field>
<Field>
<FieldLabel htmlFor="checkout-7j9-cvv">CVC</FieldLabel>
<Input id="checkout-7j9-cvv" placeholder="123" required />
</Field>
</div>
</FieldGroup>
</FieldSet>
</FieldGroup>
</CardContent>
</Card>
)
}

View File

@@ -25,10 +25,10 @@ export function DataTableViewOptions({ className }: { className?: string }) {
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-44"> <DropdownMenuContent align="end" className="w-44">
<DropdownMenuLabel className="text-muted-foreground text-sm"> <DropdownMenuLabel className="text-muted-foreground text-xs">
Exibir colunas Exibir colunas
</DropdownMenuLabel> </DropdownMenuLabel>
<DropdownMenuSeparator /> {/*<DropdownMenuSeparator />*/}
{table {table
.getAllColumns() .getAllColumns()

View File

@@ -136,7 +136,7 @@ export function NavUser({
) && ( ) && (
<> <>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuLabel className="text-muted-foreground text-sm"> <DropdownMenuLabel className="text-muted-foreground text-xs">
Aplicações Aplicações
</DropdownMenuLabel> </DropdownMenuLabel>
</> </>
@@ -155,7 +155,7 @@ export function NavUser({
</DropdownMenuGroup> </DropdownMenuGroup>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem asChild> <DropdownMenuItem asChild variant="destructive">
<Link to="/logout" className="cursor-pointer" reloadDocument> <Link to="/logout" className="cursor-pointer" reloadDocument>
<LogOutIcon /> <LogOutIcon />
Sair Sair

View File

@@ -0,0 +1,248 @@
"use client"
import { useMemo } from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
import { Separator } from "@/components/ui/separator"
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
return (
<fieldset
data-slot="field-set"
className={cn(
"flex flex-col gap-6",
"has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
className
)}
{...props}
/>
)
}
function FieldLegend({
className,
variant = "legend",
...props
}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
return (
<legend
data-slot="field-legend"
data-variant={variant}
className={cn(
"mb-3 font-medium",
"data-[variant=legend]:text-base",
"data-[variant=label]:text-sm",
className
)}
{...props}
/>
)
}
function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="field-group"
className={cn(
"group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4",
className
)}
{...props}
/>
)
}
const fieldVariants = cva(
"group/field flex w-full gap-3 data-[invalid=true]:text-destructive",
{
variants: {
orientation: {
vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"],
horizontal: [
"flex-row items-center",
"[&>[data-slot=field-label]]:flex-auto",
"has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
],
responsive: [
"flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto",
"@md/field-group:[&>[data-slot=field-label]]:flex-auto",
"@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
],
},
},
defaultVariants: {
orientation: "vertical",
},
}
)
function Field({
className,
orientation = "vertical",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
return (
<div
role="group"
data-slot="field"
data-orientation={orientation}
className={cn(fieldVariants({ orientation }), className)}
{...props}
/>
)
}
function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="field-content"
className={cn(
"group/field-content flex flex-1 flex-col gap-1.5 leading-snug",
className
)}
{...props}
/>
)
}
function FieldLabel({
className,
...props
}: React.ComponentProps<typeof Label>) {
return (
<Label
data-slot="field-label"
className={cn(
"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50",
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4",
"has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10",
className
)}
{...props}
/>
)
}
function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="field-label"
className={cn(
"flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50",
className
)}
{...props}
/>
)
}
function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
return (
<p
data-slot="field-description"
className={cn(
"text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance",
"last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5",
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
className
)}
{...props}
/>
)
}
function FieldSeparator({
children,
className,
...props
}: React.ComponentProps<"div"> & {
children?: React.ReactNode
}) {
return (
<div
data-slot="field-separator"
data-content={!!children}
className={cn(
"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
className
)}
{...props}
>
<Separator className="absolute inset-0 top-1/2" />
{children && (
<span
className="bg-background text-muted-foreground relative mx-auto block w-fit px-2"
data-slot="field-separator-content"
>
{children}
</span>
)}
</div>
)
}
function FieldError({
className,
children,
errors,
...props
}: React.ComponentProps<"div"> & {
errors?: Array<{ message?: string } | undefined>
}) {
const content = useMemo(() => {
if (children) {
return children
}
if (!errors?.length) {
return null
}
const uniqueErrors = [
...new Map(errors.map((error) => [error?.message, error])).values(),
]
if (uniqueErrors?.length == 1) {
return uniqueErrors[0]?.message
}
return (
<ul className="ml-4 flex list-disc flex-col gap-1">
{uniqueErrors.map(
(error, index) =>
error?.message && <li key={index}>{error.message}</li>
)}
</ul>
)
}, [children, errors])
if (!content) {
return null
}
return (
<div
role="alert"
data-slot="field-error"
className={cn("text-destructive text-sm font-normal", className)}
{...props}
>
{content}
</div>
)
}
export {
Field,
FieldLabel,
FieldDescription,
FieldError,
FieldGroup,
FieldLegend,
FieldSeparator,
FieldSet,
FieldContent,
FieldTitle,
}

View File

@@ -1,3 +1,5 @@
"use client"
import * as React from "react" import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label" import * as LabelPrimitive from "@radix-ui/react-label"