update
This commit is contained in:
@@ -0,0 +1,285 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user