Files
saladeaula.digital/apps/admin.saladeaula.digital/app/routes/_.$orgid.payments.$id._index/enrollments.tsx

255 lines
7.4 KiB
TypeScript

import {
BanIcon,
CheckCircle2Icon,
CircleDashedIcon,
ClockIcon,
EllipsisIcon,
HelpCircleIcon,
type LucideIcon
} from 'lucide-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 {
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 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={10} closeDelay={100}>
<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>
)
}
)}
</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 atual.'
} 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="ghost" size="icon-sm" className="cursor-pointer">
<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>
<ul className="text-sm space-y-1.5">
{seats.map(({ course, quantity }, idx) => {
return (
<li key={idx}>
{quantity}x {course.name}
</li>
)
})}
</ul>
</>
) : (
<p className="text-sm text-muted-foreground">
Nenhuma matrícula aberta foi encontrada para esta compra.
</p>
)}
</div>
</PopoverContent>
</Popover>
)
}