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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,656 @@
|
||||
import type { Route } from './+types/route'
|
||||
|
||||
import { formatCEP } from '@brazilian-utils/brazilian-utils'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useRequest, useToggle } from 'ahooks'
|
||||
import {
|
||||
AlertCircleIcon,
|
||||
ArrowLeftRightIcon,
|
||||
CircleCheckIcon,
|
||||
CircleXIcon,
|
||||
EllipsisIcon,
|
||||
ExternalLinkIcon,
|
||||
HelpCircleIcon
|
||||
} from 'lucide-react'
|
||||
import { useEffect } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { Link, useRevalidator } from 'react-router'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { Abbr } from '@repo/ui/components/abbr'
|
||||
import { Currency } from '@repo/ui/components/currency'
|
||||
import { DateTime } from '@repo/ui/components/datetime'
|
||||
import { Badge } from '@repo/ui/components/ui/badge'
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator
|
||||
} from '@repo/ui/components/ui/breadcrumb'
|
||||
import { Button } from '@repo/ui/components/ui/button'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle
|
||||
} from '@repo/ui/components/ui/card'
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger
|
||||
} from '@repo/ui/components/ui/dialog'
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
ItemContent,
|
||||
ItemGroup,
|
||||
ItemTitle
|
||||
} from '@repo/ui/components/ui/item'
|
||||
import { Kbd } from '@repo/ui/components/ui/kbd'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger
|
||||
} from '@repo/ui/components/ui/popover'
|
||||
import { Separator } from '@repo/ui/components/ui/separator'
|
||||
import { Spinner } from '@repo/ui/components/ui/spinner'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from '@repo/ui/components/ui/table'
|
||||
import { cn } from '@repo/ui/lib/utils'
|
||||
import {
|
||||
labels,
|
||||
statuses,
|
||||
type Order as Order_
|
||||
} from '@repo/ui/routes/orders/data'
|
||||
import { request as req } from '@repo/util/request'
|
||||
import {
|
||||
CreditCard,
|
||||
creditCardSchema,
|
||||
type CreditCard as CreditCardProps
|
||||
} from '../_.$orgid.enrollments.buy/payment'
|
||||
import type { Address } from '../_.$orgid.enrollments.buy/review'
|
||||
import { useWizardStore } from '../_.$orgid.enrollments.buy/store'
|
||||
import { Enrollments } from './enrollments'
|
||||
|
||||
export function meta() {
|
||||
return [
|
||||
{
|
||||
title: 'Detalhes do pagamento'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const PaymentMethodComponent = {
|
||||
PIX: PixPaymentMethod,
|
||||
BANK_SLIP: BankSlipPaymentMethod,
|
||||
CREDIT_CARD: CreditCardPaymentMethod
|
||||
}
|
||||
|
||||
type Item = {
|
||||
id: string
|
||||
name: string
|
||||
unit_price: number
|
||||
quantity: number
|
||||
}
|
||||
|
||||
type User = {
|
||||
id: string
|
||||
name: string
|
||||
email: string
|
||||
}
|
||||
|
||||
type Invoice = {
|
||||
invoice_id: string
|
||||
secure_url: string
|
||||
bank_slip?: {
|
||||
bank_slip_pdf_url: string
|
||||
}
|
||||
pix?: {
|
||||
qrcode: string
|
||||
qrcode_text: string
|
||||
}
|
||||
}
|
||||
|
||||
type Attempts = {
|
||||
sk: string
|
||||
status: string
|
||||
brand: string
|
||||
last4: string
|
||||
}
|
||||
|
||||
type Course = {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export type Enrollment = {
|
||||
status: 'PENDING' | 'EXECUTED' | 'ROLLBACK' | 'SCHEDULED'
|
||||
user: User
|
||||
course: Course
|
||||
executed_at?: string
|
||||
rollback_at?: string
|
||||
scheduled_at?: string
|
||||
reason?: string
|
||||
}
|
||||
|
||||
export type Seat = {
|
||||
course: Course
|
||||
}
|
||||
|
||||
type Order = Order_ & {
|
||||
items: Item[]
|
||||
interest_amount: number
|
||||
due_date: string
|
||||
created_at: string
|
||||
paid_at?: string
|
||||
canceled_at?: string
|
||||
expired_at?: string
|
||||
subtotal: number
|
||||
discount: number
|
||||
address: Address
|
||||
payment_attempts: Attempts[]
|
||||
credit_card?: CreditCardProps
|
||||
coupon?: string
|
||||
enrollments?: Enrollment[]
|
||||
seats?: Seat[]
|
||||
installments?: number
|
||||
created_by?: User
|
||||
invoice: Invoice
|
||||
}
|
||||
|
||||
export async function loader({ context, request, params }: Route.LoaderArgs) {
|
||||
const r = await req({
|
||||
url: `/orders/${params.id}`,
|
||||
context,
|
||||
request
|
||||
})
|
||||
|
||||
if (!r.ok) {
|
||||
throw new Response(null, { status: r.status })
|
||||
}
|
||||
|
||||
const order = (await r.json()) as Order
|
||||
return { order }
|
||||
}
|
||||
|
||||
export default function Route({ loaderData: { order } }: Route.ComponentProps) {
|
||||
const { reset } = useWizardStore()
|
||||
const {
|
||||
coupon,
|
||||
address,
|
||||
total,
|
||||
payment_method,
|
||||
interest_amount,
|
||||
discount,
|
||||
invoice,
|
||||
payment_attempts = [],
|
||||
enrollments = [],
|
||||
seats = [],
|
||||
items = [],
|
||||
created_at,
|
||||
expired_at,
|
||||
paid_at,
|
||||
subtotal
|
||||
} = order
|
||||
|
||||
const Component =
|
||||
(PaymentMethodComponent as Record<string, React.ComponentType<any>>)[
|
||||
payment_method
|
||||
] ?? UnknownPaymentMethod
|
||||
|
||||
useEffect(() => {
|
||||
reset()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="space-y-2.5">
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink asChild>
|
||||
<Link to="../payments">Pagamentos</Link>
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>Detalhes do pagamento</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
|
||||
<Card className="lg:max-w-4xl mx-auto">
|
||||
<CardHeader className="gap-0">
|
||||
<CardTitle className="text-2xl">Detalhes do pagamento</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-4">
|
||||
<ItemGroup className="grid lg:grid-cols-2 gap-4">
|
||||
<Item variant="outline" className="items-start">
|
||||
{/* Billing address */}
|
||||
<ItemContent>
|
||||
<ItemTitle>Endereço de cobrança</ItemTitle>
|
||||
<ul className="text-muted-foreground text-sm leading-normal font-normal text-balance">
|
||||
{address?.address1}
|
||||
{address?.address2 ? <>, {address?.address2}</> : null}
|
||||
<br />
|
||||
{address?.neighborhood}
|
||||
<br />
|
||||
{address?.city}, {address?.state}
|
||||
<br />
|
||||
{formatCEP(address?.postcode)}
|
||||
</ul>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
{/* Payment method */}
|
||||
<Item variant="outline" className="items-start">
|
||||
<ItemContent>
|
||||
<ItemTitle>Forma de pagamento</ItemTitle>
|
||||
<div className="text-muted-foreground text-sm leading-normal font-normal text-balance space-y-2.5">
|
||||
<div>
|
||||
{Component && <Component {...order} invoice={invoice} />}
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
{paid_at && (
|
||||
<li>
|
||||
Pago em{' '}
|
||||
<DateTime
|
||||
options={{ hour: '2-digit', minute: '2-digit' }}
|
||||
>
|
||||
{paid_at}
|
||||
</DateTime>
|
||||
</li>
|
||||
)}
|
||||
|
||||
{expired_at && (
|
||||
<li>
|
||||
Expirado em{' '}
|
||||
<DateTime
|
||||
options={{ hour: '2-digit', minute: '2-digit' }}
|
||||
>
|
||||
{expired_at}
|
||||
</DateTime>
|
||||
</li>
|
||||
)}
|
||||
|
||||
<li>
|
||||
Comprado em{' '}
|
||||
<DateTime
|
||||
options={{ hour: '2-digit', minute: '2-digit' }}
|
||||
>
|
||||
{created_at}
|
||||
</DateTime>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</ItemContent>
|
||||
|
||||
{payment_attempts.length > 0 ? (
|
||||
<ItemActions>
|
||||
<PaymentAttemptsMenu payment_attempts={payment_attempts} />
|
||||
</ItemActions>
|
||||
) : null}
|
||||
</Item>
|
||||
</ItemGroup>
|
||||
|
||||
<Table className="pointer-events-none">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Curso</TableHead>
|
||||
<TableHead>Quantidade</TableHead>
|
||||
<TableHead>Valor unit.</TableHead>
|
||||
<TableHead>Total</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{items?.map(({ name, unit_price, quantity }, index) => {
|
||||
return (
|
||||
<TableRow key={index}>
|
||||
<TableCell>{name}</TableCell>
|
||||
<TableCell>{quantity}</TableCell>
|
||||
<TableCell>
|
||||
<Currency>{unit_price}</Currency>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Currency>{unit_price * quantity}</Currency>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
</TableBody>
|
||||
|
||||
{/* Summary */}
|
||||
<TableFooter>
|
||||
<TableRow>
|
||||
<TableCell className="text-right" colSpan={3}>
|
||||
Subtotal
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Currency>{subtotal}</Currency>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/* Discount */}
|
||||
<TableRow>
|
||||
<TableCell colSpan={3}>
|
||||
<span className="flex gap-1 justify-end">
|
||||
Descontos
|
||||
{coupon && (
|
||||
<Kbd>
|
||||
<Abbr maxLen={8}>{coupon}</Abbr>
|
||||
</Kbd>
|
||||
)}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Currency>{discount}</Currency>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/* Interest */}
|
||||
{interest_amount ? (
|
||||
<TableRow>
|
||||
<TableCell className="text-right" colSpan={3}>
|
||||
Juros
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Currency>{interest_amount}</Currency>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{/* Total */}
|
||||
<TableRow>
|
||||
<TableCell className="text-right" colSpan={3}>
|
||||
Total
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Currency>{total}</Currency>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Enrollments enrollments={enrollments} seats={seats} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
type PaymentMethodProps = {
|
||||
id: string
|
||||
status: string
|
||||
total: number
|
||||
invoice: Invoice
|
||||
installments: number
|
||||
}
|
||||
|
||||
type BankSlipPaymentMethodProps = PaymentMethodProps & {}
|
||||
|
||||
function BankSlipPaymentMethod({
|
||||
status,
|
||||
invoice
|
||||
}: BankSlipPaymentMethodProps) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<ul className="flex gap-x-1.5">
|
||||
<li>Boleto bancário</li>
|
||||
<li>
|
||||
<Status status={status} />
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{invoice?.bank_slip ? (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
className="cursor-pointer"
|
||||
asChild
|
||||
>
|
||||
<a href={invoice.bank_slip.bank_slip_pdf_url} target="_blank">
|
||||
<ExternalLinkIcon /> Abrir o boleto bancário
|
||||
</a>
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type PixPaymentMethodrops = PaymentMethodProps & {}
|
||||
|
||||
function PixPaymentMethod({ invoice, status }: PixPaymentMethodrops) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<ul className="flex gap-x-1.5">
|
||||
<li>Pix</li>
|
||||
<li>
|
||||
<Status status={status} />
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{invoice?.pix ? (
|
||||
<div
|
||||
className="font-mono text-xs break-all p-2.5 border
|
||||
rounded-md text-red-900 dark:text-yellow-600
|
||||
bg-gray-50 dark:bg-muted/50 select-all"
|
||||
>
|
||||
{invoice.pix.qrcode_text}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function UnknownPaymentMethod() {
|
||||
return <>Deposito bancário</>
|
||||
}
|
||||
|
||||
type CreditCardPaymentMethodProps = PaymentMethodProps & {
|
||||
stats?: { last_attempt_succeeded: boolean }
|
||||
credit_card: { last4: string; brand: string }
|
||||
}
|
||||
|
||||
function CreditCardPaymentMethod({
|
||||
id,
|
||||
status,
|
||||
total,
|
||||
credit_card,
|
||||
installments,
|
||||
invoice,
|
||||
stats
|
||||
}: CreditCardPaymentMethodProps) {
|
||||
return (
|
||||
<>
|
||||
<ul className="lg:flex gap-x-1">
|
||||
<li>
|
||||
<Abbr maxLen={6}>{credit_card.brand}</Abbr>
|
||||
</li>
|
||||
<li>(Crédito) **** {credit_card.last4}</li>
|
||||
<li>
|
||||
{stats?.last_attempt_succeeded === false && status === 'PENDING' ? (
|
||||
<Badge variant="outline" className="text-red-400 px-1.5">
|
||||
<AlertCircleIcon /> Negado
|
||||
</Badge>
|
||||
) : (
|
||||
<Status status={status} />
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
{installments}x <Currency>{total / Number(installments)}</Currency>
|
||||
</p>
|
||||
|
||||
{stats?.last_attempt_succeeded === false &&
|
||||
status === 'PENDING' &&
|
||||
invoice?.invoice_id ? (
|
||||
<div className="lg:flex justify-center mt-2">
|
||||
<CreditCardPaymentDialog
|
||||
id={id}
|
||||
total={total}
|
||||
installments={installments}
|
||||
invoice_id={invoice.invoice_id}
|
||||
>
|
||||
<Button size="sm" variant="secondary" className="cursor-pointer">
|
||||
<ArrowLeftRightIcon /> Pagar com outro cartão
|
||||
</Button>
|
||||
</CreditCardPaymentDialog>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const formSchema = z.object({
|
||||
credit_card: creditCardSchema
|
||||
})
|
||||
|
||||
type Schema = z.input<typeof formSchema>
|
||||
|
||||
function CreditCardPaymentDialog({
|
||||
children,
|
||||
id,
|
||||
installments,
|
||||
invoice_id,
|
||||
total
|
||||
}) {
|
||||
const revalidator = useRevalidator()
|
||||
const [open, { set: setOpen, toggle }] = useToggle()
|
||||
const { runAsync } = useRequest(
|
||||
async ({ credit_card }) => {
|
||||
return await fetch(`/~/api/orders/${id}/payment-retries`, {
|
||||
method: 'POST',
|
||||
headers: new Headers({ 'Content-Type': 'application/json' }),
|
||||
body: JSON.stringify({ credit_card, installments, invoice_id })
|
||||
})
|
||||
},
|
||||
{ manual: true }
|
||||
)
|
||||
const { control, handleSubmit, formState } = useForm<Schema>({
|
||||
resolver: zodResolver(formSchema)
|
||||
})
|
||||
|
||||
const onSubmit = async ({ credit_card }: Schema) => {
|
||||
await runAsync({ credit_card })
|
||||
revalidator.revalidate()
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={toggle}>
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Novo cartão de crédito</DialogTitle>
|
||||
<DialogDescription>
|
||||
Use um novo cartão para concluir o pagamento. Nenhuma cobrança foi
|
||||
realizada no cartão anterior.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||
<CreditCard control={control} />
|
||||
|
||||
<Separator />
|
||||
|
||||
<DialogFooter className="*:cursor-pointer">
|
||||
<DialogClose asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="link"
|
||||
className="text-black dark:text-white"
|
||||
tabIndex={-1}
|
||||
>
|
||||
Cancelar
|
||||
</Button>
|
||||
</DialogClose>
|
||||
|
||||
<Button type="submit" disabled={formState.isSubmitting}>
|
||||
{formState.isSubmitting && <Spinner />}
|
||||
Pagar <Currency>{total}</Currency>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
function PaymentAttemptsMenu({
|
||||
payment_attempts
|
||||
}: {
|
||||
payment_attempts: Attempts[]
|
||||
}) {
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="secondary" className="cursor-pointer" size="icon-sm">
|
||||
<EllipsisIcon />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end" className="w-82 space-y-1.5">
|
||||
{payment_attempts.map(({ sk, brand, last4, status }, index) => {
|
||||
const [, , created_at] = sk.split('#')
|
||||
|
||||
return (
|
||||
<ul key={index} className="text-sm flex gap-1.5">
|
||||
<li>
|
||||
<Kbd>
|
||||
<DateTime
|
||||
options={{
|
||||
year: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
}}
|
||||
>
|
||||
{created_at}
|
||||
</DateTime>
|
||||
</Kbd>
|
||||
</li>
|
||||
<li>
|
||||
<Abbr maxLen={6}>{brand}</Abbr>
|
||||
</li>
|
||||
<li className="ml-auto">**** {last4}</li>
|
||||
<li className="flex items-center">
|
||||
{status === 'FAILED' ? (
|
||||
<CircleXIcon className="size-4 text-red-400" />
|
||||
) : (
|
||||
<CircleCheckIcon className="size-4 text-green-400" />
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
)
|
||||
})}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user