add retries to order
This commit is contained in:
@@ -1,23 +1,14 @@
|
||||
import { useForm, Controller, useWatch, type Control } from 'react-hook-form'
|
||||
import { PatternFormat } from 'react-number-format'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { ErrorMessage } from '@hookform/error-message'
|
||||
import { z } from 'zod'
|
||||
import { ArrowRightIcon, CircleQuestionMarkIcon } from 'lucide-react'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import valid from 'card-validator'
|
||||
import { ArrowRightIcon, CircleQuestionMarkIcon } from 'lucide-react'
|
||||
import { Controller, useForm, useWatch, type Control } from 'react-hook-form'
|
||||
import { PatternFormat } from 'react-number-format'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { Currency } from '@repo/ui/components/currency'
|
||||
import { Button } from '@repo/ui/components/ui/button'
|
||||
import { Kbd } from '@repo/ui/components/ui/kbd'
|
||||
import { Label } from '@repo/ui/components/ui/label'
|
||||
import { RadioGroup, RadioGroupItem } from '@repo/ui/components/ui/radio-group'
|
||||
import { Separator } from '@repo/ui/components/ui/separator'
|
||||
import { Input } from '@repo/ui/components/ui/input'
|
||||
import { Card, CardContent } from '@repo/ui/components/ui/card'
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardTrigger
|
||||
} from '@repo/ui/components/ui/hover-card'
|
||||
import {
|
||||
Field,
|
||||
FieldError,
|
||||
@@ -25,18 +16,27 @@ import {
|
||||
FieldLabel,
|
||||
FieldSet
|
||||
} from '@repo/ui/components/ui/field'
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardTrigger
|
||||
} from '@repo/ui/components/ui/hover-card'
|
||||
import { Input } from '@repo/ui/components/ui/input'
|
||||
import { Kbd } from '@repo/ui/components/ui/kbd'
|
||||
import { Label } from '@repo/ui/components/ui/label'
|
||||
import {
|
||||
NativeSelect,
|
||||
NativeSelectOption
|
||||
} from '@repo/ui/components/ui/native-select'
|
||||
import { Currency } from '@repo/ui/components/currency'
|
||||
import { RadioGroup, RadioGroupItem } from '@repo/ui/components/ui/radio-group'
|
||||
import { Separator } from '@repo/ui/components/ui/separator'
|
||||
|
||||
import { useWizard } from '@/components/wizard'
|
||||
import { isName } from '../_.$orgid.users.add/data'
|
||||
import { applyDiscount } from './discount'
|
||||
|
||||
import { useWizardStore } from './store'
|
||||
|
||||
const creditCard = z.object({
|
||||
export const creditCardSchema = z.object({
|
||||
holder_name: z
|
||||
.string()
|
||||
.trim()
|
||||
@@ -71,7 +71,7 @@ const formSchema = z.discriminatedUnion(
|
||||
|
||||
z.object({
|
||||
payment_method: z.literal('CREDIT_CARD'),
|
||||
credit_card: creditCard,
|
||||
credit_card: creditCardSchema,
|
||||
installments: z.coerce.number().int().min(1).max(12)
|
||||
})
|
||||
],
|
||||
@@ -80,7 +80,7 @@ const formSchema = z.discriminatedUnion(
|
||||
|
||||
type Schema = z.input<typeof formSchema>
|
||||
|
||||
export type CreditCard = z.infer<typeof creditCard>
|
||||
export type CreditCard = z.infer<typeof creditCardSchema>
|
||||
|
||||
export function Payment({}) {
|
||||
const wizard = useWizard()
|
||||
@@ -95,6 +95,7 @@ export function Payment({}) {
|
||||
resolver: zodResolver(formSchema)
|
||||
})
|
||||
const paymentMethod = useWatch({ control, name: 'payment_method' })
|
||||
const total = subtotal + discount
|
||||
|
||||
const onSubmit = async ({ payment_method, ...data }: Schema) => {
|
||||
if (payment_method === 'CREDIT_CARD') {
|
||||
@@ -156,191 +157,9 @@ export function Payment({}) {
|
||||
/>
|
||||
|
||||
{paymentMethod === 'CREDIT_CARD' ? (
|
||||
<CreditCard control={control} total={subtotal + discount} />
|
||||
) : null}
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="flex justify-between gap-4 *:cursor-pointer">
|
||||
<Button
|
||||
type="button"
|
||||
variant="link"
|
||||
className="text-black dark:text-white"
|
||||
onClick={() => wizard('cart')}
|
||||
tabIndex={-1}
|
||||
>
|
||||
Voltar
|
||||
</Button>
|
||||
|
||||
<Button type="submit" variant="secondary">
|
||||
Continuar <ArrowRightIcon />
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export function CreditCard({
|
||||
total,
|
||||
control
|
||||
}: {
|
||||
total: number
|
||||
control: Control<Schema>
|
||||
}) {
|
||||
const currentYear = new Date().getFullYear()
|
||||
const years = Array.from({ length: 10 }, (_, i) => currentYear + i)
|
||||
|
||||
return (
|
||||
<Card className="lg:w-1/2">
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<FieldSet>
|
||||
{/* Credir card number */}
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="credit_card.number"
|
||||
render={({ field: { onChange, ref, ...field }, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<FieldLabel htmlFor={field.name}>Número do cartão</FieldLabel>
|
||||
<PatternFormat
|
||||
id={field.name}
|
||||
format="#### #### #### ####"
|
||||
mask="_"
|
||||
placeholder="•••• •••• •••• ••••"
|
||||
customInput={Input}
|
||||
getInputRef={ref}
|
||||
aria-invalid={fieldState.invalid}
|
||||
onValueChange={({ value }) => {
|
||||
onChange(value)
|
||||
}}
|
||||
{...field}
|
||||
/>
|
||||
|
||||
{fieldState.invalid && (
|
||||
<FieldError errors={[fieldState.error]} />
|
||||
)}
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
{/* Holder name */}
|
||||
<Controller
|
||||
control={control}
|
||||
name="credit_card.holder_name"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<FieldLabel htmlFor={field.name}>Nome do titular</FieldLabel>
|
||||
<Input
|
||||
id={field.name}
|
||||
aria-invalid={fieldState.invalid}
|
||||
{...field}
|
||||
/>
|
||||
{fieldState.invalid && (
|
||||
<FieldError errors={[fieldState.error]} />
|
||||
)}
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FieldSet className="grid grid-cols-3 gap-4">
|
||||
<Controller
|
||||
control={control}
|
||||
name="credit_card.exp_month"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<FieldLabel htmlFor={field.name}>Mês</FieldLabel>
|
||||
<NativeSelect
|
||||
id={field.name}
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
aria-invalid={fieldState.invalid}
|
||||
>
|
||||
<NativeSelectOption value="" disabled>
|
||||
Selecione
|
||||
</NativeSelectOption>
|
||||
|
||||
{Array.from({ length: 12 }, (_, i) => {
|
||||
const v = String(i + 1).padStart(2, '0')
|
||||
return (
|
||||
<NativeSelectOption key={v} value={v}>
|
||||
{v}
|
||||
</NativeSelectOption>
|
||||
)
|
||||
})}
|
||||
</NativeSelect>
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="credit_card.exp_year"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<FieldLabel htmlFor={field.name}>Ano</FieldLabel>
|
||||
<NativeSelect
|
||||
id={field.name}
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
aria-invalid={fieldState.invalid}
|
||||
>
|
||||
<NativeSelectOption value="" disabled>
|
||||
Selecione
|
||||
</NativeSelectOption>
|
||||
|
||||
{years.map((year) => (
|
||||
<NativeSelectOption key={year} value={String(year)}>
|
||||
{year}
|
||||
</NativeSelectOption>
|
||||
))}
|
||||
</NativeSelect>
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="credit_card.cvv"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<FieldLabel htmlFor={field.name}>
|
||||
CVC
|
||||
<HoverCard openDelay={0}>
|
||||
<HoverCardTrigger asChild>
|
||||
<button type="button" tabIndex={-1}>
|
||||
<CircleQuestionMarkIcon className="size-4 text-muted-foreground" />
|
||||
</button>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent
|
||||
align="end"
|
||||
className="text-sm space-y-1.5 lg:w-78"
|
||||
>
|
||||
<p>
|
||||
O <Kbd>CVC</Kbd> é o código de segurança do cartão
|
||||
de crédito.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Ele fica no verso do cartão e geralmente possui{' '}
|
||||
<Kbd>3 dígitos</Kbd> (ou <Kbd>4 dígitos</Kbd> na
|
||||
frente, no caso do American Express).
|
||||
</p>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
</FieldLabel>
|
||||
<Input
|
||||
id={field.name}
|
||||
aria-invalid={fieldState.invalid}
|
||||
{...field}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
</FieldSet>
|
||||
<Card className="lg:w-1/2">
|
||||
<CardContent className="space-y-6">
|
||||
<CreditCard control={control} />
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
@@ -384,10 +203,182 @@ export function CreditCard({
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="flex justify-between gap-4 *:cursor-pointer">
|
||||
<Button
|
||||
type="button"
|
||||
variant="link"
|
||||
className="text-black dark:text-white"
|
||||
onClick={() => wizard('cart')}
|
||||
tabIndex={-1}
|
||||
>
|
||||
Voltar
|
||||
</Button>
|
||||
|
||||
<Button type="submit" variant="secondary">
|
||||
Continuar <ArrowRightIcon />
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export function CreditCard({ control }: { control: Control<Schema> }) {
|
||||
const currentYear = new Date().getFullYear()
|
||||
const years = Array.from({ length: 10 }, (_, i) => currentYear + i)
|
||||
|
||||
return (
|
||||
<FieldGroup>
|
||||
<FieldSet>
|
||||
{/* Credir card number */}
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="credit_card.number"
|
||||
render={({ field: { onChange, ref, ...field }, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<FieldLabel htmlFor={field.name}>Número do cartão</FieldLabel>
|
||||
<PatternFormat
|
||||
id={field.name}
|
||||
format="#### #### #### ####"
|
||||
mask="_"
|
||||
placeholder="•••• •••• •••• ••••"
|
||||
customInput={Input}
|
||||
getInputRef={ref}
|
||||
aria-invalid={fieldState.invalid}
|
||||
onValueChange={({ value }) => {
|
||||
onChange(value)
|
||||
}}
|
||||
{...field}
|
||||
/>
|
||||
|
||||
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
{/* Holder name */}
|
||||
<Controller
|
||||
control={control}
|
||||
name="credit_card.holder_name"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<FieldLabel htmlFor={field.name}>Nome do titular</FieldLabel>
|
||||
<Input
|
||||
id={field.name}
|
||||
aria-invalid={fieldState.invalid}
|
||||
{...field}
|
||||
/>
|
||||
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FieldSet className="grid grid-cols-3 gap-4">
|
||||
<Controller
|
||||
control={control}
|
||||
name="credit_card.exp_month"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<FieldLabel htmlFor={field.name}>Mês</FieldLabel>
|
||||
<NativeSelect
|
||||
id={field.name}
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
aria-invalid={fieldState.invalid}
|
||||
>
|
||||
<NativeSelectOption value="" disabled>
|
||||
Selecione
|
||||
</NativeSelectOption>
|
||||
|
||||
{Array.from({ length: 12 }, (_, i) => {
|
||||
const v = String(i + 1).padStart(2, '0')
|
||||
return (
|
||||
<NativeSelectOption key={v} value={v}>
|
||||
{v}
|
||||
</NativeSelectOption>
|
||||
)
|
||||
})}
|
||||
</NativeSelect>
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="credit_card.exp_year"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<FieldLabel htmlFor={field.name}>Ano</FieldLabel>
|
||||
<NativeSelect
|
||||
id={field.name}
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
aria-invalid={fieldState.invalid}
|
||||
>
|
||||
<NativeSelectOption value="" disabled>
|
||||
Selecione
|
||||
</NativeSelectOption>
|
||||
|
||||
{years.map((year) => (
|
||||
<NativeSelectOption key={year} value={String(year)}>
|
||||
{year}
|
||||
</NativeSelectOption>
|
||||
))}
|
||||
</NativeSelect>
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="credit_card.cvv"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
<FieldLabel htmlFor={field.name}>
|
||||
CVC
|
||||
<HoverCard openDelay={0}>
|
||||
<HoverCardTrigger asChild>
|
||||
<button type="button" tabIndex={-1}>
|
||||
<CircleQuestionMarkIcon className="size-4 text-muted-foreground" />
|
||||
</button>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent
|
||||
align="end"
|
||||
className="text-sm space-y-1.5 lg:w-78"
|
||||
>
|
||||
<p>
|
||||
O <Kbd>CVC</Kbd> é o código de segurança do cartão de
|
||||
crédito.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Ele fica no verso do cartão e geralmente possui{' '}
|
||||
<Kbd>3 dígitos</Kbd> (ou <Kbd>4 dígitos</Kbd> na frente,
|
||||
no caso do American Express).
|
||||
</p>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
</FieldLabel>
|
||||
<Input
|
||||
id={field.name}
|
||||
aria-invalid={fieldState.invalid}
|
||||
{...field}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
</FieldSet>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,34 @@
|
||||
import { formatCEP } from '@brazilian-utils/brazilian-utils'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useRequest, useToggle } from 'ahooks'
|
||||
import { PatternFormat } from 'react-number-format'
|
||||
import valid from 'card-validator'
|
||||
import { ExternalLinkIcon, PencilIcon, SearchIcon } from 'lucide-react'
|
||||
import { Controller, useForm } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { formatCEP } from '@brazilian-utils/brazilian-utils'
|
||||
import { PatternFormat } from 'react-number-format'
|
||||
import { z } from 'zod'
|
||||
import valid from 'card-validator'
|
||||
|
||||
import { Currency } from '@repo/ui/components/currency'
|
||||
import { Kbd } from '@repo/ui/components/ui/kbd'
|
||||
import { Abbr } from '@repo/ui/components/abbr'
|
||||
import { Currency } from '@repo/ui/components/currency'
|
||||
import { Button } from '@repo/ui/components/ui/button'
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger
|
||||
} from '@repo/ui/components/ui/dialog'
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemGroup,
|
||||
ItemTitle
|
||||
} from '@repo/ui/components/ui/item'
|
||||
import { Kbd } from '@repo/ui/components/ui/kbd'
|
||||
import { Separator } from '@repo/ui/components/ui/separator'
|
||||
import { Spinner } from '@repo/ui/components/ui/spinner'
|
||||
import {
|
||||
@@ -22,24 +40,6 @@ import {
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from '@repo/ui/components/ui/table'
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemGroup,
|
||||
ItemTitle
|
||||
} from '@repo/ui/components/ui/item'
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger
|
||||
} from '@repo/ui/components/ui/dialog'
|
||||
import { paymentMethods } from '@repo/ui/routes/orders/data'
|
||||
|
||||
import { useWizard } from '@/components/wizard'
|
||||
@@ -59,8 +59,6 @@ import {
|
||||
InputGroupInput
|
||||
} from '@repo/ui/components/ui/input-group'
|
||||
|
||||
import { useWizardStore } from './store'
|
||||
import { useParams } from 'react-router'
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
@@ -68,6 +66,8 @@ import {
|
||||
EmptyHeader,
|
||||
EmptyTitle
|
||||
} from '@repo/ui/components/ui/empty'
|
||||
import { useParams } from 'react-router'
|
||||
import { useWizardStore } from './store'
|
||||
|
||||
type ReviewProps = {
|
||||
onSubmit: () => void | Promise<void>
|
||||
@@ -445,7 +445,7 @@ export function AddressDialog({ children }) {
|
||||
tabIndex={-1}
|
||||
className="text-black dark:text-white"
|
||||
>
|
||||
Cancel
|
||||
Cancelar
|
||||
</Button>
|
||||
</DialogClose>
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { create } from 'zustand'
|
||||
import { persist } from 'zustand/middleware'
|
||||
import { applyDiscount, type Coupon } from './discount'
|
||||
import { calcInterest, type CreditCard } from './payment'
|
||||
|
||||
import type { PaymentMethod } from '@repo/ui/routes/orders/data'
|
||||
|
||||
import type { Enrollment } from '../_.$orgid.enrollments.add/data'
|
||||
import type { Item } from './bulk'
|
||||
import { applyDiscount, type Coupon } from './discount'
|
||||
import { calcInterest, type CreditCard } from './payment'
|
||||
import type { Address } from './review'
|
||||
|
||||
export type WizardState = {
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import type { Route } from './+types/route'
|
||||
|
||||
import { formatCEP } from '@brazilian-utils/brazilian-utils'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useRequest } from 'ahooks'
|
||||
import {
|
||||
AlertCircleIcon,
|
||||
ArrowLeftRightIcon,
|
||||
HelpCircleIcon
|
||||
} from 'lucide-react'
|
||||
import { useEffect } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { Link } from 'react-router'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { Currency } from '@repo/ui/components/currency'
|
||||
import {
|
||||
@@ -52,7 +56,23 @@ import {
|
||||
type Order as Order_
|
||||
} from '@repo/ui/routes/orders/data'
|
||||
|
||||
import type { CreditCard as CreditCard_ } from '../_.$orgid.enrollments.buy/payment'
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger
|
||||
} from '@repo/ui/components/ui/dialog'
|
||||
import { Separator } from '@repo/ui/components/ui/separator'
|
||||
import { Spinner } from '@repo/ui/components/ui/spinner'
|
||||
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'
|
||||
|
||||
@@ -65,12 +85,12 @@ export function meta() {
|
||||
}
|
||||
|
||||
const PaymentMethodComponent = {
|
||||
PIX: Pix,
|
||||
BANK_SLIP: BankSlip,
|
||||
CREDIT_CARD: CreditCard
|
||||
PIX: PixPaymentMethod,
|
||||
BANK_SLIP: BankSlipPaymentMethod,
|
||||
CREDIT_CARD: CreditCardPaymentMethod
|
||||
}
|
||||
|
||||
type Item_ = {
|
||||
type Item = {
|
||||
id: string
|
||||
name: string
|
||||
unit_price: number
|
||||
@@ -82,18 +102,24 @@ type User = {
|
||||
name: string
|
||||
}
|
||||
|
||||
type Invoice = {
|
||||
invoice_id: string
|
||||
secure_url: string
|
||||
}
|
||||
|
||||
type Order = Order_ & {
|
||||
items: Item_[]
|
||||
items: Item[]
|
||||
interest_amount: number
|
||||
due_date: string
|
||||
created_at: string
|
||||
subtotal: number
|
||||
discount: number
|
||||
address: Address
|
||||
credit_card?: CreditCard_
|
||||
credit_card?: CreditCardProps
|
||||
coupon?: string
|
||||
installments?: number
|
||||
created_by?: User
|
||||
invoice: Invoice
|
||||
}
|
||||
|
||||
export async function loader({ context, request, params }: Route.LoaderArgs) {
|
||||
@@ -115,11 +141,15 @@ export default function Route({ loaderData: { order } }: Route.ComponentProps) {
|
||||
payment_method,
|
||||
interest_amount,
|
||||
discount,
|
||||
invoice,
|
||||
subtotal,
|
||||
items = []
|
||||
} = order
|
||||
|
||||
const Component = PaymentMethodComponent[payment_method]
|
||||
const Component =
|
||||
(PaymentMethodComponent as Record<string, React.ComponentType<any>>)[
|
||||
payment_method
|
||||
] ?? UnknownPaymentMethod
|
||||
|
||||
useEffect(() => {
|
||||
reset()
|
||||
@@ -168,7 +198,9 @@ export default function Route({ loaderData: { order } }: Route.ComponentProps) {
|
||||
<ItemContent>
|
||||
<ItemTitle>Forma de pagamento</ItemTitle>
|
||||
<div className="text-muted-foreground text-sm leading-normal font-normal text-balance">
|
||||
{Component && <Component {...order} />}
|
||||
{Component && (
|
||||
<Component {...order} invoice_id={invoice['invoice_id']} />
|
||||
)}
|
||||
</div>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
@@ -271,23 +303,50 @@ function Status({ status: s }: { status: string }) {
|
||||
}
|
||||
|
||||
type PaymentMethodProps = {
|
||||
id: string
|
||||
status: string
|
||||
total: number
|
||||
invoice_id: string
|
||||
installments: number
|
||||
}
|
||||
|
||||
type CreditCardProps = PaymentMethodProps & {
|
||||
type BankSlipPaymentMethodProps = PaymentMethodProps & {}
|
||||
|
||||
function BankSlipPaymentMethod({ status }: BankSlipPaymentMethodProps) {
|
||||
return (
|
||||
<ul className="flex max-lg:flex-col gap-x-1.5">
|
||||
<li>Boleto bancário</li>
|
||||
<li>
|
||||
<Status status={status} />
|
||||
</li>
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
type PixPaymentMethodrops = PaymentMethodProps & {}
|
||||
|
||||
function PixPaymentMethod({}: PixPaymentMethodrops) {
|
||||
return <>Pix</>
|
||||
}
|
||||
|
||||
function UnknownPaymentMethod() {
|
||||
return <>Deposito bancário</>
|
||||
}
|
||||
|
||||
type CreditCardPaymentMethodProps = PaymentMethodProps & {
|
||||
stats: { last_attempt_succeeded: boolean }
|
||||
credit_card: { last4: string; brand: string }
|
||||
}
|
||||
|
||||
function CreditCard({
|
||||
function CreditCardPaymentMethod({
|
||||
id,
|
||||
status,
|
||||
total,
|
||||
credit_card,
|
||||
installments,
|
||||
invoice_id,
|
||||
stats
|
||||
}: CreditCardProps) {
|
||||
}: CreditCardPaymentMethodProps) {
|
||||
return (
|
||||
<>
|
||||
<ul className="flex max-lg:flex-col gap-x-1.5">
|
||||
@@ -295,12 +354,12 @@ function CreditCard({
|
||||
{credit_card.brand} (Crédito) **** {credit_card.last4}
|
||||
</li>
|
||||
<li>
|
||||
{!stats.last_attempt_succeeded ? (
|
||||
{stats.last_attempt_succeeded === false ? (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-red-400 border-red-400 px-1.5"
|
||||
>
|
||||
<AlertCircleIcon /> Transação negada
|
||||
<AlertCircleIcon /> Pagamento não aprovado
|
||||
</Badge>
|
||||
) : (
|
||||
<Status status={status} />
|
||||
@@ -312,32 +371,94 @@ function CreditCard({
|
||||
{installments}x <Currency>{total / Number(installments)}</Currency>
|
||||
</p>
|
||||
|
||||
{!stats.last_attempt_succeeded ? (
|
||||
{stats.last_attempt_succeeded === false ? (
|
||||
<div className="flex justify-center mt-2">
|
||||
<Button size="sm" variant="secondary" className="cursor-pointer">
|
||||
<ArrowLeftRightIcon /> Tentar com outro cartão
|
||||
</Button>
|
||||
<CreditCardPaymentDialog
|
||||
id={id}
|
||||
total={total}
|
||||
installments={installments}
|
||||
invoice_id={invoice_id}
|
||||
>
|
||||
<Button size="sm" variant="secondary" className="cursor-pointer">
|
||||
<ArrowLeftRightIcon /> Pagar com outro cartão
|
||||
</Button>
|
||||
</CreditCardPaymentDialog>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
type BankSlipProps = PaymentMethodProps & {}
|
||||
const formSchema = z.object({
|
||||
credit_card: creditCardSchema
|
||||
})
|
||||
|
||||
type Schema = z.input<typeof formSchema>
|
||||
|
||||
function CreditCardPaymentDialog({
|
||||
children,
|
||||
id,
|
||||
installments,
|
||||
invoice_id,
|
||||
total
|
||||
}) {
|
||||
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) => {
|
||||
const r = await runAsync({ credit_card })
|
||||
console.log(r.ok)
|
||||
console.log(await r.json())
|
||||
}
|
||||
|
||||
function BankSlip({ status }: BankSlipProps) {
|
||||
return (
|
||||
<ul className="flex max-lg:flex-col gap-x-1.5">
|
||||
<li>Boleto bancário</li>
|
||||
<li>
|
||||
<Status status={status} />
|
||||
</li>
|
||||
</ul>
|
||||
<Dialog>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
type PixProps = PaymentMethodProps & {}
|
||||
|
||||
function Pix({}: PixProps) {
|
||||
return <>Pix</>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user