add zustard
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Fragment } from 'react'
|
||||
import { Fragment, useEffect } from 'react'
|
||||
import {
|
||||
Trash2Icon,
|
||||
PlusIcon,
|
||||
@@ -10,6 +10,7 @@ import { useParams } from 'react-router'
|
||||
import { ErrorMessage } from '@hookform/error-message'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { z } from 'zod'
|
||||
import { DateTime } from 'luxon'
|
||||
|
||||
import { Form } from '@repo/ui/components/ui/form'
|
||||
import {
|
||||
@@ -30,7 +31,6 @@ import {
|
||||
import {
|
||||
MAX_ITEMS,
|
||||
formSchema,
|
||||
type Enrollment,
|
||||
type Course,
|
||||
type User
|
||||
} from '../_.$orgid.enrollments.add/data'
|
||||
@@ -41,7 +41,7 @@ import { UserPicker } from '../_.$orgid.enrollments.add/user-picker'
|
||||
import { Summary } from './bulk'
|
||||
import { currency } from './utils'
|
||||
import { useWizard } from '@/components/wizard'
|
||||
import type { Item } from './bulk'
|
||||
import { useWizardStore } from './store'
|
||||
|
||||
const emptyRow = {
|
||||
user: undefined,
|
||||
@@ -49,42 +49,24 @@ const emptyRow = {
|
||||
scheduled_for: undefined
|
||||
}
|
||||
|
||||
const formSchemaAssigned = formSchema.extend({
|
||||
coupon: z
|
||||
.object({
|
||||
code: z.string(),
|
||||
type: z.enum(['FIXED', 'PERCENT']),
|
||||
amount: z.number().positive()
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
|
||||
type Schema = z.infer<typeof formSchemaAssigned>
|
||||
type Schema = z.infer<typeof formSchema>
|
||||
|
||||
type AssignedProps = {
|
||||
onSubmit: (value: any) => void | Promise<void>
|
||||
courses: Promise<{ hits: Course[] }>
|
||||
enrollments: Enrollment[]
|
||||
coupon?: object
|
||||
}
|
||||
|
||||
export function Assigned({
|
||||
courses,
|
||||
onSubmit,
|
||||
enrollments,
|
||||
coupon: couponInit
|
||||
}: AssignedProps) {
|
||||
export function Assigned({ courses }: AssignedProps) {
|
||||
const wizard = useWizard()
|
||||
const { orgid } = useParams()
|
||||
const { update, ...state } = useWizardStore()
|
||||
const form = useForm({
|
||||
resolver: zodResolver(formSchemaAssigned),
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
coupon: couponInit,
|
||||
enrollments: enrollments.length
|
||||
? enrollments.map((e: any) => ({
|
||||
enrollments: state.enrollments.length
|
||||
? state.enrollments.map((e: any) => ({
|
||||
...e,
|
||||
scheduled_for: e.scheduled_for
|
||||
? new Date(e.scheduled_for)
|
||||
? DateTime.fromISO(e.scheduled_for, { zone: 'local' }).toJSDate()
|
||||
: undefined
|
||||
}))
|
||||
: [emptyRow]
|
||||
@@ -96,15 +78,10 @@ export function Assigned({
|
||||
control,
|
||||
name: 'enrollments'
|
||||
})
|
||||
const items = useWatch({
|
||||
const enrollments = useWatch({
|
||||
control,
|
||||
name: 'enrollments'
|
||||
})
|
||||
const coupon = useWatch({ control, name: 'coupon' })
|
||||
const subtotal = items.reduce(
|
||||
(acc, { course }) => acc + (course?.unit_price || 0),
|
||||
0
|
||||
)
|
||||
|
||||
const onSearch = async (search: string) => {
|
||||
const params = new URLSearchParams({ q: search })
|
||||
@@ -113,24 +90,22 @@ export function Assigned({
|
||||
return hits
|
||||
}
|
||||
|
||||
const onSubmit_ = async ({ enrollments, coupon }: Schema) => {
|
||||
const items = Object.values(
|
||||
enrollments.reduce<Record<string, Item>>((acc, e) => {
|
||||
const id = e.course.id
|
||||
|
||||
acc[id] ??= { course: e.course, quantity: 0 }
|
||||
acc[id].quantity++
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
)
|
||||
await onSubmit({ enrollments, items, coupon })
|
||||
const onSubmit = async ({ enrollments }: Schema) => {
|
||||
update({ enrollments })
|
||||
wizard('payment')
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const parsed = formSchema.safeParse({ enrollments })
|
||||
|
||||
if (parsed.success) {
|
||||
update(parsed.data)
|
||||
}
|
||||
}, [enrollments])
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={handleSubmit(onSubmit_)} className="space-y-4">
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||
<div className="grid w-full gap-3 lg:grid-cols-[3fr_3fr_2fr_2fr_auto]">
|
||||
{/* Header */}
|
||||
<>
|
||||
@@ -166,7 +141,9 @@ export function Assigned({
|
||||
|
||||
{/* Rows */}
|
||||
{fields.map((field, index) => {
|
||||
const { unit_price } = items?.[index]?.course || { unit_price: 0 }
|
||||
const { unit_price } = enrollments?.[index]?.course || {
|
||||
unit_price: 0
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment key={field.id}>
|
||||
@@ -281,7 +258,7 @@ export function Assigned({
|
||||
</div>
|
||||
|
||||
{/* Summary */}
|
||||
<Summary {...{ subtotal, coupon, setValue }} />
|
||||
<Summary />
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Fragment } from 'react'
|
||||
import { Fragment, useEffect } from 'react'
|
||||
import {
|
||||
ArrowRightIcon,
|
||||
MinusIcon,
|
||||
@@ -36,9 +36,11 @@ import { MAX_ITEMS, type Course } from '../_.$orgid.enrollments.add/data'
|
||||
import { Discount, applyDiscount, type Coupon } from './discount'
|
||||
import { currency } from './utils'
|
||||
import { useWizard } from '@/components/wizard'
|
||||
import { useWizardStore } from './store'
|
||||
|
||||
const emptyRow = {
|
||||
course: undefined
|
||||
course: undefined as any,
|
||||
quantity: 1
|
||||
}
|
||||
|
||||
const item = z.object({
|
||||
@@ -57,39 +59,24 @@ const item = z.object({
|
||||
})
|
||||
|
||||
const formSchema = z.object({
|
||||
items: z.array(item).min(1).max(MAX_ITEMS),
|
||||
coupon: z
|
||||
.object({
|
||||
code: z.string(),
|
||||
type: z.enum(['FIXED', 'PERCENT']),
|
||||
amount: z.number().positive()
|
||||
})
|
||||
.optional()
|
||||
items: z.array(item).min(1).max(MAX_ITEMS)
|
||||
})
|
||||
|
||||
type Schema = z.infer<typeof formSchema>
|
||||
type Schema = z.input<typeof formSchema>
|
||||
|
||||
export type Item = z.infer<typeof item>
|
||||
|
||||
type BulkProps = {
|
||||
onSubmit: (value: any) => void | Promise<void>
|
||||
courses: Promise<{ hits: Course[] }>
|
||||
items: Item[]
|
||||
coupon?: Coupon
|
||||
}
|
||||
|
||||
export function Bulk({
|
||||
courses,
|
||||
onSubmit,
|
||||
items: itemsInit,
|
||||
coupon: couponInit
|
||||
}: BulkProps) {
|
||||
export function Bulk({ courses }: BulkProps) {
|
||||
const wizard = useWizard()
|
||||
const { update, ...state } = useWizardStore()
|
||||
const form = useForm({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
items: itemsInit?.length ? itemsInit : [emptyRow],
|
||||
coupon: couponInit
|
||||
items: state.items.length ? state.items : [emptyRow]
|
||||
}
|
||||
})
|
||||
const {
|
||||
@@ -109,35 +96,20 @@ export function Bulk({
|
||||
control,
|
||||
name: 'items'
|
||||
})
|
||||
const coupon = useWatch({ control, name: 'coupon' })
|
||||
|
||||
const subtotal = items.reduce(
|
||||
(acc, { course, quantity }) =>
|
||||
acc +
|
||||
(course?.unit_price || 0) *
|
||||
(Number.isFinite(quantity) && quantity > 0 ? quantity : 1),
|
||||
0
|
||||
)
|
||||
|
||||
const onSubmit_ = async ({ coupon, ...data }: Schema) => {
|
||||
const items = Object.values(
|
||||
data.items.reduce<Record<string, Item>>((acc, item) => {
|
||||
const id = item.course.id
|
||||
|
||||
if (!acc[id]) {
|
||||
acc[id] = { ...item }
|
||||
} else {
|
||||
acc[id].quantity += item.quantity
|
||||
}
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
)
|
||||
|
||||
await onSubmit({ coupon, items, enrollments: [] })
|
||||
const onSubmit_ = async ({ items }: Schema) => {
|
||||
update({ items })
|
||||
wizard('payment')
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const parsed = formSchema.safeParse({ items })
|
||||
|
||||
if (parsed.success) {
|
||||
update(parsed.data)
|
||||
}
|
||||
}, [items])
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={handleSubmit(onSubmit_)} className="space-y-4">
|
||||
@@ -316,7 +288,7 @@ export function Bulk({
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Summary {...{ subtotal, coupon, setValue }} />
|
||||
<Summary />
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
@@ -337,17 +309,9 @@ export function Bulk({
|
||||
)
|
||||
}
|
||||
|
||||
type SummaryProps = {
|
||||
subtotal: number
|
||||
coupon?: Coupon
|
||||
setValue: UseFormSetValue<any>
|
||||
}
|
||||
|
||||
export function Summary({ subtotal, coupon, setValue }: SummaryProps) {
|
||||
const discount = coupon
|
||||
? applyDiscount(subtotal, coupon.amount, coupon.type) * -1
|
||||
: 0
|
||||
const total = subtotal > 0 ? subtotal + discount : 0
|
||||
export function Summary() {
|
||||
const { summary, coupon, update } = useWizardStore()
|
||||
const { total, discount, subtotal } = summary()
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -407,7 +371,7 @@ export function Summary({ subtotal, coupon, setValue }: SummaryProps) {
|
||||
tabIndex={-1}
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setValue('coupon', null)
|
||||
update({ coupon: undefined })
|
||||
}}
|
||||
>
|
||||
<XIcon />
|
||||
@@ -416,7 +380,7 @@ export function Summary({ subtotal, coupon, setValue }: SummaryProps) {
|
||||
<Discount
|
||||
disabled={subtotal === 0}
|
||||
onChange={(coupon) => {
|
||||
setValue('coupon', coupon)
|
||||
update({ coupon })
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -72,8 +72,6 @@ export function Discount({ onChange, ...props }: DiscountProps) {
|
||||
discount_type: 'FIXED' | 'PERCENT'
|
||||
}
|
||||
|
||||
console.log(code)
|
||||
|
||||
onChange?.({ code, amount, type })
|
||||
|
||||
reset()
|
||||
|
||||
@@ -34,8 +34,9 @@ import { Currency } from '@repo/ui/components/currency'
|
||||
import { useWizard } from '@/components/wizard'
|
||||
import { isName } from '../_.$orgid.users.add/data'
|
||||
import type { PaymentMethod } from '@repo/ui/routes/orders/data'
|
||||
import type { WizardState } from './route'
|
||||
import type { WizardState } from './store'
|
||||
import { applyDiscount } from './discount'
|
||||
import { useWizardStore } from './store'
|
||||
|
||||
const creditCard = z.object({
|
||||
holder_name: z
|
||||
@@ -55,49 +56,42 @@ const creditCard = z.object({
|
||||
cvv: z.string().min(3).max(4)
|
||||
})
|
||||
|
||||
const formSchema = z.discriminatedUnion('payment_method', [
|
||||
z.object({
|
||||
payment_method: z.literal('PIX')
|
||||
}),
|
||||
const formSchema = z.discriminatedUnion(
|
||||
'payment_method',
|
||||
[
|
||||
z.object({
|
||||
payment_method: z.literal('PIX')
|
||||
}),
|
||||
|
||||
z.object({
|
||||
payment_method: z.literal('BANK_SLIP')
|
||||
}),
|
||||
z.object({
|
||||
payment_method: z.literal('BANK_SLIP')
|
||||
}),
|
||||
|
||||
z.object({
|
||||
payment_method: z.literal('MANUAL')
|
||||
}),
|
||||
z.object({
|
||||
payment_method: z.literal('MANUAL')
|
||||
}),
|
||||
|
||||
z.object({
|
||||
payment_method: z.literal('CREDIT_CARD'),
|
||||
credit_card: creditCard,
|
||||
installments: z.coerce.number().int().min(1).max(12)
|
||||
})
|
||||
])
|
||||
z.object({
|
||||
payment_method: z.literal('CREDIT_CARD'),
|
||||
credit_card: creditCard,
|
||||
installments: z.coerce.number().int().min(1).max(12)
|
||||
})
|
||||
],
|
||||
{ error: 'Escolha uma forma de pagamento' }
|
||||
)
|
||||
|
||||
type Schema = z.input<typeof formSchema>
|
||||
|
||||
export type CreditCard = z.infer<typeof creditCard>
|
||||
|
||||
type PaymentProps = {
|
||||
state: WizardState
|
||||
onSubmit: (value: any) => void | Promise<void>
|
||||
payment_method?: PaymentMethod
|
||||
credit_card?: CreditCard
|
||||
}
|
||||
|
||||
export function Payment({
|
||||
onSubmit,
|
||||
state,
|
||||
payment_method: paymentMethodInit,
|
||||
credit_card: creditCardInit = undefined
|
||||
}: PaymentProps) {
|
||||
export function Payment({}) {
|
||||
const wizard = useWizard()
|
||||
const { update, ...state } = useWizardStore()
|
||||
const { control, handleSubmit } = useForm<Schema>({
|
||||
defaultValues: {
|
||||
payment_method: paymentMethodInit,
|
||||
installments: state?.installments ?? 1,
|
||||
credit_card: creditCardInit
|
||||
payment_method: state.payment_method,
|
||||
installments: state.installments,
|
||||
credit_card: state.credit_card
|
||||
},
|
||||
resolver: zodResolver(formSchema)
|
||||
})
|
||||
@@ -114,13 +108,23 @@ export function Payment({
|
||||
: 0
|
||||
const total = subtotal > 0 ? subtotal + discount : 0
|
||||
|
||||
const onSubmit_ = async (data: Schema) => {
|
||||
await onSubmit({ credit_card: undefined, ...data })
|
||||
const onSubmit = async ({ payment_method, ...data }: Schema) => {
|
||||
if (payment_method === 'CREDIT_CARD') {
|
||||
// @ts-ignore
|
||||
update({ payment_method, ...data })
|
||||
} else {
|
||||
update({
|
||||
payment_method,
|
||||
installments: undefined,
|
||||
credit_card: undefined
|
||||
})
|
||||
}
|
||||
|
||||
wizard('review')
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit_)} className="space-y-4">
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||
<Controller
|
||||
name="payment_method"
|
||||
control={control}
|
||||
@@ -206,6 +210,7 @@ export function CreditCard({
|
||||
{/* Credir card number */}
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="credit_card.number"
|
||||
render={({ field: { onChange, ref, ...field }, fieldState }) => (
|
||||
<Field data-invalid={fieldState.invalid}>
|
||||
|
||||
@@ -37,11 +37,11 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger
|
||||
} from '@repo/ui/components/ui/dialog'
|
||||
import { paymentMethods } from '@repo/ui/routes/orders/data'
|
||||
|
||||
import { useWizard } from '@/components/wizard'
|
||||
import { type WizardState } from './route'
|
||||
import { type WizardState } from './store'
|
||||
import { applyDiscount } from './discount'
|
||||
import { paymentMethods } from '@repo/ui/routes/orders/data'
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
@@ -57,46 +57,28 @@ import {
|
||||
InputGroupButton,
|
||||
InputGroupInput
|
||||
} from '@repo/ui/components/ui/input-group'
|
||||
import { calcInterest } from './payment'
|
||||
|
||||
import { useWizardStore } from './store'
|
||||
|
||||
type ReviewProps = {
|
||||
state: WizardState
|
||||
onSubmit: (value: WizardState) => void | Promise<void>
|
||||
}
|
||||
|
||||
export function Review({ state, onSubmit }: ReviewProps) {
|
||||
export function Review({ onSubmit }: ReviewProps) {
|
||||
const wizard = useWizard()
|
||||
const { items, summary } = useWizardStore()
|
||||
const { subtotal, discount, interest_amount, total } = summary()
|
||||
const [loading, { set }] = useToggle()
|
||||
const { coupon, items } = state || { items: [], coupon: {} }
|
||||
const subtotal =
|
||||
items?.reduce(
|
||||
(acc, { course, quantity }) =>
|
||||
acc +
|
||||
(course?.unit_price || 0) *
|
||||
(Number.isFinite(quantity) && quantity > 0 ? quantity : 1),
|
||||
0
|
||||
) || 0
|
||||
const discount = coupon
|
||||
? applyDiscount(subtotal, coupon.amount, coupon.type) * -1
|
||||
: 0
|
||||
const total = subtotal > 0 ? subtotal + discount : 0
|
||||
const installments = state.installments
|
||||
const interest_amount =
|
||||
state.payment_method === 'CREDIT_CARD' &&
|
||||
typeof installments === 'number' &&
|
||||
installments > 1
|
||||
? calcInterest(total, installments) - total
|
||||
: 0
|
||||
|
||||
return (
|
||||
<>
|
||||
<Address total={total} {...state} />
|
||||
<Summary />
|
||||
|
||||
<form
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault()
|
||||
set(true)
|
||||
await onSubmit(state)
|
||||
// await onSubmit(state)
|
||||
}}
|
||||
className="space-y-4"
|
||||
>
|
||||
@@ -164,7 +146,7 @@ export function Review({ state, onSubmit }: ReviewProps) {
|
||||
Total
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Currency>{total + interest_amount}</Currency>
|
||||
<Currency>{total}</Currency>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
@@ -193,11 +175,10 @@ export function Review({ state, onSubmit }: ReviewProps) {
|
||||
)
|
||||
}
|
||||
|
||||
export function Address({
|
||||
total,
|
||||
payment_method,
|
||||
credit_card
|
||||
}: WizardState & { total: number }) {
|
||||
export function Summary() {
|
||||
const { summary, credit_card, payment_method, installments } =
|
||||
useWizardStore()
|
||||
const { total } = summary()
|
||||
const numberValidation = valid.number(credit_card?.number)
|
||||
|
||||
return (
|
||||
@@ -229,7 +210,8 @@ export function Address({
|
||||
{numberValidation.card?.niceType} (Crédito) ****{' '}
|
||||
{credit_card.number.slice(-4)}
|
||||
<br />
|
||||
1x <Currency>{total}</Currency>
|
||||
{installments}x{' '}
|
||||
<Currency>{total / Number(installments)}</Currency>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { Route } from './+types/route'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useFetcher } from 'react-router'
|
||||
import { Link } from 'react-router'
|
||||
import { useLocalStorageState, useMount } from 'ahooks'
|
||||
import { useMount } from 'ahooks'
|
||||
import { BookSearchIcon, CircleCheckBigIcon, WalletIcon } from 'lucide-react'
|
||||
|
||||
import {
|
||||
@@ -25,39 +25,17 @@ import { createSearch } from '@repo/util/meili'
|
||||
import { cloudflareContext } from '@repo/auth/context'
|
||||
import { Label } from '@repo/ui/components/ui/label'
|
||||
import { Skeleton } from '@repo/ui/components/skeleton'
|
||||
import type { PaymentMethod } from '@repo/ui/routes/orders/data'
|
||||
|
||||
import { Wizard, WizardStep } from '@/components/wizard'
|
||||
import { Step, StepItem, StepSeparator } from '@/components/step'
|
||||
import type { Course, Enrollment } from '../_.$orgid.enrollments.add/data'
|
||||
import { Wizard, WizardStep } from '@/components/wizard'
|
||||
import type { Course } from '../_.$orgid.enrollments.add/data'
|
||||
import { Bulk } from './bulk'
|
||||
import { Payment } from './payment'
|
||||
import { Assigned } from './assigned'
|
||||
import { Bulk, type Item } from './bulk'
|
||||
import { Payment, type CreditCard } from './payment'
|
||||
import { Review } from './review'
|
||||
import type { Coupon } from './discount'
|
||||
import { useFetcher } from 'react-router'
|
||||
|
||||
export type WizardState = {
|
||||
index: number
|
||||
kind: 'bulk' | 'assigned'
|
||||
items: Item[]
|
||||
enrollments: Enrollment[]
|
||||
coupon?: Coupon
|
||||
installments?: number
|
||||
payment_method?: PaymentMethod
|
||||
credit_card?: CreditCard
|
||||
}
|
||||
|
||||
const emptyWizard: WizardState = {
|
||||
index: 0,
|
||||
kind: 'bulk',
|
||||
items: [],
|
||||
enrollments: [],
|
||||
coupon: undefined,
|
||||
payment_method: undefined,
|
||||
installments: undefined,
|
||||
credit_card: undefined
|
||||
}
|
||||
import { useWizardStore, type WizardState } from './store'
|
||||
import { useState } from 'react'
|
||||
|
||||
export function meta({}: Route.MetaArgs) {
|
||||
return [{ title: 'Comprar matrículas' }]
|
||||
@@ -87,20 +65,7 @@ export default function Route({
|
||||
}: Route.ComponentProps) {
|
||||
const fetcher = useFetcher()
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const [state, setState] = useLocalStorageState<WizardState>('wizard_cart', {
|
||||
defaultValue: emptyWizard
|
||||
})
|
||||
const index = state.index
|
||||
const kind = state.kind
|
||||
const props = {
|
||||
...state,
|
||||
courses,
|
||||
onSubmit: async (data: any) =>
|
||||
setState((prev) => ({
|
||||
...(prev ?? emptyWizard),
|
||||
...data
|
||||
}))
|
||||
}
|
||||
const { index, kind, setIndex, setKind } = useWizardStore()
|
||||
|
||||
const onSubmit = async (data: WizardState) => {
|
||||
await fetcher.submit(JSON.stringify(data), {
|
||||
@@ -157,15 +122,7 @@ export default function Route({
|
||||
</StepItem>
|
||||
</Step>
|
||||
|
||||
<Wizard
|
||||
index={index}
|
||||
onChange={(nextIndex) =>
|
||||
setState((prev) => ({
|
||||
...(prev ?? emptyWizard),
|
||||
index: nextIndex
|
||||
}))
|
||||
}
|
||||
>
|
||||
<Wizard index={index} onChange={setIndex}>
|
||||
{/* Cart */}
|
||||
<WizardStep name="cart">
|
||||
<Label
|
||||
@@ -187,40 +144,27 @@ export default function Route({
|
||||
<Switch
|
||||
checked={kind === 'assigned'}
|
||||
onCheckedChange={(checked) =>
|
||||
setState((prev) => ({
|
||||
...(prev ?? emptyWizard),
|
||||
kind: checked ? 'assigned' : 'bulk'
|
||||
}))
|
||||
setKind(checked ? 'assigned' : 'bulk')
|
||||
}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
</Label>
|
||||
|
||||
{kind == 'assigned' ? (
|
||||
<Assigned {...props} />
|
||||
{kind === 'assigned' ? (
|
||||
<Assigned courses={courses} />
|
||||
) : (
|
||||
<Bulk {...props} />
|
||||
<Bulk courses={courses} />
|
||||
)}
|
||||
</WizardStep>
|
||||
|
||||
{/* Payment */}
|
||||
<WizardStep name="payment">
|
||||
<Payment
|
||||
state={state}
|
||||
payment_method={state.payment_method}
|
||||
credit_card={state.credit_card}
|
||||
onSubmit={(data: any) => {
|
||||
setState((prev) => ({
|
||||
...(prev ?? emptyWizard),
|
||||
...data
|
||||
}))
|
||||
}}
|
||||
/>
|
||||
<Payment />
|
||||
</WizardStep>
|
||||
|
||||
{/* Review */}
|
||||
<WizardStep name="review">
|
||||
<Review state={state} onSubmit={onSubmit} />
|
||||
<Review onSubmit={onSubmit} />
|
||||
</WizardStep>
|
||||
</Wizard>
|
||||
</CardContent>
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
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'
|
||||
|
||||
export type WizardState = {
|
||||
index: number
|
||||
kind: 'bulk' | 'assigned'
|
||||
items: Item[]
|
||||
enrollments: Enrollment[]
|
||||
coupon?: Coupon
|
||||
installments?: number
|
||||
payment_method?: PaymentMethod
|
||||
credit_card?: CreditCard
|
||||
}
|
||||
|
||||
type Summary = {
|
||||
subtotal: number
|
||||
discount: number
|
||||
interest_amount: number
|
||||
total: number
|
||||
}
|
||||
|
||||
export type WizardStore = WizardState & {
|
||||
setIndex: (index: number) => void
|
||||
setKind: (kind: 'bulk' | 'assigned') => void
|
||||
update: (data: Partial<WizardState>) => void
|
||||
reset: () => void
|
||||
summary: () => Summary
|
||||
}
|
||||
|
||||
const emptyWizard: WizardState = {
|
||||
index: 0,
|
||||
kind: 'bulk',
|
||||
items: [],
|
||||
enrollments: []
|
||||
}
|
||||
|
||||
export const useWizardStore = create<WizardStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
...emptyWizard,
|
||||
|
||||
setIndex: (index) => set({ index }),
|
||||
setKind: (kind) => set({ kind }),
|
||||
|
||||
summary: () => {
|
||||
const { items, coupon, credit_card, installments } = get()
|
||||
const subtotal = items.reduce(
|
||||
(acc, { course, quantity }) =>
|
||||
acc +
|
||||
(course?.unit_price || 0) *
|
||||
(Number.isFinite(quantity) && quantity > 0 ? quantity : 1),
|
||||
0
|
||||
)
|
||||
const discount = coupon
|
||||
? applyDiscount(subtotal, coupon.amount, coupon.type) * -1
|
||||
: 0
|
||||
|
||||
const total = subtotal > 0 ? subtotal + discount : 0
|
||||
const interest_amount =
|
||||
credit_card && typeof installments === 'number' && installments > 1
|
||||
? calcInterest(total, installments) - total
|
||||
: 0
|
||||
|
||||
return {
|
||||
subtotal,
|
||||
discount,
|
||||
interest_amount,
|
||||
total: total + interest_amount
|
||||
}
|
||||
},
|
||||
|
||||
update: (data) =>
|
||||
set((state) => {
|
||||
if (data.enrollments) {
|
||||
const items = Object.values(
|
||||
data.enrollments.reduce<Record<string, Item>>(
|
||||
(acc, { course }) => {
|
||||
const { id } = course
|
||||
|
||||
acc[id] ??= { course, quantity: 0 }
|
||||
acc[id].quantity++
|
||||
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
)
|
||||
|
||||
return {
|
||||
...state,
|
||||
...data,
|
||||
items
|
||||
}
|
||||
}
|
||||
|
||||
if (data.items) {
|
||||
const items = Object.values(
|
||||
data.items.reduce<Record<string, Item>>((acc, item) => {
|
||||
const id = item.course.id
|
||||
|
||||
if (!acc[id]) {
|
||||
acc[id] = { ...item }
|
||||
} else {
|
||||
acc[id].quantity += item.quantity
|
||||
}
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
)
|
||||
|
||||
return {
|
||||
...state,
|
||||
...data,
|
||||
items,
|
||||
enrollments: []
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
...data
|
||||
}
|
||||
}),
|
||||
|
||||
reset: () => set({ ...emptyWizard })
|
||||
}),
|
||||
{
|
||||
name: 'wizard_cart'
|
||||
}
|
||||
)
|
||||
)
|
||||
Reference in New Issue
Block a user