286 lines
8.5 KiB
TypeScript
286 lines
8.5 KiB
TypeScript
import {
|
|
BanIcon,
|
|
CheckCircle2Icon,
|
|
CircleDashedIcon,
|
|
ClockIcon,
|
|
EllipsisIcon,
|
|
HelpCircleIcon,
|
|
PlusIcon,
|
|
type LucideIcon
|
|
} from 'lucide-react'
|
|
import { Fragment } from 'react'
|
|
|
|
import { Abbr } from '@repo/ui/components/abbr'
|
|
import { DateTime } from '@repo/ui/components/datetime'
|
|
import { Avatar, AvatarFallback } from '@repo/ui/components/ui/avatar'
|
|
import { Badge } from '@repo/ui/components/ui/badge'
|
|
import { Button } from '@repo/ui/components/ui/button'
|
|
import {
|
|
Card,
|
|
CardAction,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle
|
|
} from '@repo/ui/components/ui/card'
|
|
import {
|
|
HoverCard,
|
|
HoverCardContent,
|
|
HoverCardTrigger
|
|
} from '@repo/ui/components/ui/hover-card'
|
|
import { Kbd } from '@repo/ui/components/ui/kbd'
|
|
import {
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger
|
|
} from '@repo/ui/components/ui/popover'
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow
|
|
} from '@repo/ui/components/ui/table'
|
|
import { cn, initials } from '@repo/ui/lib/utils'
|
|
import { Link } from 'react-router'
|
|
import type { Enrollment, Seat } from './route'
|
|
|
|
const dtOptions: Intl.DateTimeFormatOptions = {
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
}
|
|
|
|
export function Enrollments({
|
|
enrollments,
|
|
seats
|
|
}: {
|
|
enrollments: Enrollment[]
|
|
seats: Seat[]
|
|
}) {
|
|
return (
|
|
<Card className="lg:max-w-4xl mx-auto">
|
|
<CardHeader>
|
|
<CardTitle className="text-xl">Matrículas relacionadas</CardTitle>
|
|
<CardDescription>
|
|
Acompanhe os detalhes de todas as matrículas relacionadas a esta
|
|
compra.
|
|
</CardDescription>
|
|
|
|
<CardAction>
|
|
<SeatsMenu seats={seats} />
|
|
</CardAction>
|
|
</CardHeader>
|
|
|
|
<CardContent>
|
|
<Table>
|
|
<TableHeader className="pointer-events-none">
|
|
<TableRow>
|
|
<TableHead>Colaborador</TableHead>
|
|
<TableHead>Curso</TableHead>
|
|
<TableHead>Status</TableHead>
|
|
<TableHead>Executada em</TableHead>
|
|
<TableHead>Revogada em</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody className="[&_tr]:hover:bg-transparent">
|
|
{enrollments.map(
|
|
(
|
|
{
|
|
user,
|
|
course,
|
|
status,
|
|
executed_at,
|
|
rollback_at,
|
|
reason: reason_
|
|
},
|
|
idx
|
|
) => {
|
|
const friendlyReason = reason_ ? reason(reason_) : null
|
|
|
|
return (
|
|
<TableRow key={idx}>
|
|
<TableCell>
|
|
<div className="flex gap-2.5 items-center">
|
|
<Avatar className="size-10 hidden lg:block">
|
|
<AvatarFallback className="border">
|
|
{initials(user.name)}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
|
|
<ul>
|
|
<li className="font-bold">
|
|
<Abbr>{user.name}</Abbr>
|
|
</li>
|
|
<li className="text-muted-foreground text-sm">
|
|
<Abbr>{user.email}</Abbr>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Abbr>{course.name}</Abbr>
|
|
</TableCell>
|
|
<TableCell>
|
|
{friendlyReason ? (
|
|
<HoverCard openDelay={0}>
|
|
<HoverCardTrigger>
|
|
<Status status={status} />
|
|
</HoverCardTrigger>
|
|
<HoverCardContent align="end" className="text-sm">
|
|
<p className="flex gap-1">
|
|
<HelpCircleIcon className="size-4.5 mt-px" />{' '}
|
|
{friendlyReason}
|
|
</p>
|
|
</HoverCardContent>
|
|
</HoverCard>
|
|
) : (
|
|
<Status status={status} />
|
|
)}
|
|
</TableCell>
|
|
<TableCell>
|
|
{executed_at ? (
|
|
<DateTime options={dtOptions}>{executed_at}</DateTime>
|
|
) : null}
|
|
</TableCell>
|
|
|
|
<TableCell>
|
|
{rollback_at ? (
|
|
<DateTime options={dtOptions}>{rollback_at}</DateTime>
|
|
) : null}
|
|
</TableCell>
|
|
</TableRow>
|
|
)
|
|
}
|
|
)}
|
|
|
|
{enrollments.length === 0 && (
|
|
<TableRow>
|
|
<TableCell className="text-center h-24" colSpan={5}>
|
|
Nenhuma matrícula ainda.
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
const reasons: Record<string, string> = {
|
|
DEDUPLICATION: 'Matrícula ou agendamento já existentes.',
|
|
CANCELLATION: 'Cancelamento da matrícula.',
|
|
UNSCHEDULED: 'Cancelamento do agendamento da matrícula.',
|
|
DEADLINE: 'Data do agendamento é anterior ao dia do pagamento.'
|
|
} as const
|
|
|
|
const reason = (reason_: string): string | null => {
|
|
return reason_ in reasons ? reasons[reason_] : null
|
|
}
|
|
|
|
const statuses: Record<string, { icon: LucideIcon; color?: string }> = {
|
|
PENDING: {
|
|
icon: CircleDashedIcon,
|
|
color: 'text-blue-400 [&_svg]:text-blue-500'
|
|
},
|
|
SCHEDULED: {
|
|
icon: ClockIcon,
|
|
color: 'text-blue-400 [&_svg]:text-blue-500'
|
|
},
|
|
EXECUTED: {
|
|
icon: CheckCircle2Icon,
|
|
color: 'text-green-400 [&_svg]:text-green-500'
|
|
},
|
|
ROLLBACK: {
|
|
icon: BanIcon,
|
|
color: 'text-orange-400 [&_svg]:text-orange-500'
|
|
}
|
|
}
|
|
|
|
const labels: Record<string, string> = {
|
|
PENDING: 'Pendente',
|
|
EXECUTED: 'Executada',
|
|
SCHEDULED: 'Agendada',
|
|
ROLLBACK: 'Revogada'
|
|
}
|
|
|
|
function Status({ status: s }: { status: string }) {
|
|
const status = labels[s] ?? s
|
|
const { icon: Icon, color } = statuses?.[s] ?? { icon: HelpCircleIcon }
|
|
|
|
return (
|
|
<Badge variant="outline" className={cn(color, 'px-1.5')}>
|
|
<Icon className={cn('stroke-2', color)} />
|
|
{status}
|
|
</Badge>
|
|
)
|
|
}
|
|
|
|
function SeatsMenu({ seats: seats_ }: { seats: Seat[] }) {
|
|
const seats = Object.values(
|
|
seats_.reduce((acc: any, { course }) => {
|
|
acc[course.id] ??= { course, quantity: 0 }
|
|
acc[course.id].quantity++
|
|
return acc
|
|
}, {})
|
|
) as { course: Seat['course']; quantity: number }[]
|
|
|
|
return (
|
|
<Popover>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
variant="secondary"
|
|
size="icon-sm"
|
|
className="cursor-pointer relative"
|
|
>
|
|
{seats.length > 0 && (
|
|
<span className="absolute flex size-2 -top-0.5 -right-0.5">
|
|
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-400 opacity-75"></span>
|
|
<span className="relative inline-flex size-2 rounded-full bg-green-500"></span>
|
|
</span>
|
|
)}
|
|
<EllipsisIcon />
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent align="end" className="w-82">
|
|
<div className="grid gap-4">
|
|
{seats.length > 0 ? (
|
|
<>
|
|
<div className="space-y-2">
|
|
<h4 className="leading-none font-medium">Matrículas abertas</h4>
|
|
<p className="text-muted-foreground text-sm">
|
|
Matrículas que estão abertas e relacionadas a esta compra.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="text-sm grid grid-cols-[1fr_15%] gap-1">
|
|
{seats.map(({ course, quantity }, idx) => {
|
|
return (
|
|
<Fragment key={idx}>
|
|
<Abbr>{course.name}</Abbr>
|
|
<div className="flex justify-end">
|
|
<Kbd>{quantity}x</Kbd>
|
|
</div>
|
|
</Fragment>
|
|
)
|
|
})}
|
|
</div>
|
|
|
|
<Button size="sm" variant="outline" asChild>
|
|
<Link to="../enrollments/seats">
|
|
<PlusIcon /> Matricular
|
|
</Link>
|
|
</Button>
|
|
</>
|
|
) : (
|
|
<p className="text-sm text-muted-foreground">
|
|
Nenhuma matrícula aberta foi encontrada para esta compra.
|
|
</p>
|
|
)}
|
|
</div>
|
|
</PopoverContent>
|
|
</Popover>
|
|
)
|
|
}
|