add zustard
This commit is contained in:
@@ -50,6 +50,7 @@ app.include_router(users.password, prefix='/users')
|
|||||||
app.include_router(orders.router, prefix='/orders')
|
app.include_router(orders.router, prefix='/orders')
|
||||||
app.include_router(orders.checkout, prefix='/orders')
|
app.include_router(orders.checkout, prefix='/orders')
|
||||||
app.include_router(orgs.add, prefix='/orgs')
|
app.include_router(orgs.add, prefix='/orgs')
|
||||||
|
app.include_router(orgs.address, prefix='/orgs')
|
||||||
app.include_router(orgs.admins, prefix='/orgs')
|
app.include_router(orgs.admins, prefix='/orgs')
|
||||||
app.include_router(orgs.billing, prefix='/orgs')
|
app.include_router(orgs.billing, prefix='/orgs')
|
||||||
app.include_router(orgs.custom_pricing, prefix='/orgs')
|
app.include_router(orgs.custom_pricing, prefix='/orgs')
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from .add import router as add
|
from .add import router as add
|
||||||
|
from .address import router as address
|
||||||
from .admins import router as admins
|
from .admins import router as admins
|
||||||
from .billing import router as billing
|
from .billing import router as billing
|
||||||
from .custom_pricing import router as custom_pricing
|
from .custom_pricing import router as custom_pricing
|
||||||
@@ -9,6 +10,7 @@ from .users.batch_jobs import router as batch_jobs
|
|||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'add',
|
'add',
|
||||||
|
'address',
|
||||||
'admins',
|
'admins',
|
||||||
'billing',
|
'billing',
|
||||||
'custom_pricing',
|
'custom_pricing',
|
||||||
|
|||||||
22
api.saladeaula.digital/tests/routes/orgs/test_address.py
Normal file
22
api.saladeaula.digital/tests/routes/orgs/test_address.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
from http import HTTPMethod, HTTPStatus
|
||||||
|
|
||||||
|
from layercake.dynamodb import DynamoDBPersistenceLayer
|
||||||
|
|
||||||
|
from ...conftest import HttpApiProxy, LambdaContext
|
||||||
|
|
||||||
|
|
||||||
|
def test_address(
|
||||||
|
app,
|
||||||
|
seeds,
|
||||||
|
http_api_proxy: HttpApiProxy,
|
||||||
|
dynamodb_persistence_layer: DynamoDBPersistenceLayer,
|
||||||
|
lambda_context: LambdaContext,
|
||||||
|
):
|
||||||
|
r = app.lambda_handler(
|
||||||
|
http_api_proxy(
|
||||||
|
raw_path='/orgs/cJtK9SsnJhKPyxESe7g3DG/address',
|
||||||
|
method=HTTPMethod.GET,
|
||||||
|
),
|
||||||
|
lambda_context,
|
||||||
|
)
|
||||||
|
assert r['statusCode'] == HTTPStatus.OK
|
||||||
@@ -20,4 +20,4 @@ export const labels: Record<string, string> = {
|
|||||||
CLOSED: 'Fechado'
|
CLOSED: 'Fechado'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const tz = 'America/Sao_Paulo'
|
export const tz = 'local'
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ export default function Route({
|
|||||||
<DateTime>
|
<DateTime>
|
||||||
{LuxonDateTime.fromISO(
|
{LuxonDateTime.fromISO(
|
||||||
output.scheduled_for,
|
output.scheduled_for,
|
||||||
{ zone: 'America/Sao_Paulo' }
|
{ zone: 'local' }
|
||||||
).toJSDate()}
|
).toJSDate()}
|
||||||
{}
|
{}
|
||||||
</DateTime>
|
</DateTime>
|
||||||
@@ -239,7 +239,11 @@ export default function Route({
|
|||||||
>
|
>
|
||||||
<li>
|
<li>
|
||||||
<CalendarIcon className="size-3.5" />
|
<CalendarIcon className="size-3.5" />
|
||||||
{datetime.format(new Date(sk))}
|
<DateTime
|
||||||
|
options={{ hour: '2-digit', minute: '2-digit' }}
|
||||||
|
>
|
||||||
|
{sk}
|
||||||
|
</DateTime>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<UserIcon className="size-3.5" /> {created_by.name}
|
<UserIcon className="size-3.5" /> {created_by.name}
|
||||||
@@ -256,14 +260,6 @@ export default function Route({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const datetime = new Intl.DateTimeFormat('pt-BR', {
|
|
||||||
day: '2-digit',
|
|
||||||
month: '2-digit',
|
|
||||||
year: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
})
|
|
||||||
|
|
||||||
function NotFound() {
|
function NotFound() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Fragment } from 'react'
|
import { Fragment, useEffect } from 'react'
|
||||||
import {
|
import {
|
||||||
Trash2Icon,
|
Trash2Icon,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
@@ -10,6 +10,7 @@ import { useParams } from 'react-router'
|
|||||||
import { ErrorMessage } from '@hookform/error-message'
|
import { ErrorMessage } from '@hookform/error-message'
|
||||||
import { zodResolver } from '@hookform/resolvers/zod'
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import { DateTime } from 'luxon'
|
||||||
|
|
||||||
import { Form } from '@repo/ui/components/ui/form'
|
import { Form } from '@repo/ui/components/ui/form'
|
||||||
import {
|
import {
|
||||||
@@ -30,7 +31,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
MAX_ITEMS,
|
MAX_ITEMS,
|
||||||
formSchema,
|
formSchema,
|
||||||
type Enrollment,
|
|
||||||
type Course,
|
type Course,
|
||||||
type User
|
type User
|
||||||
} from '../_.$orgid.enrollments.add/data'
|
} from '../_.$orgid.enrollments.add/data'
|
||||||
@@ -41,7 +41,7 @@ import { UserPicker } from '../_.$orgid.enrollments.add/user-picker'
|
|||||||
import { Summary } from './bulk'
|
import { Summary } from './bulk'
|
||||||
import { currency } from './utils'
|
import { currency } from './utils'
|
||||||
import { useWizard } from '@/components/wizard'
|
import { useWizard } from '@/components/wizard'
|
||||||
import type { Item } from './bulk'
|
import { useWizardStore } from './store'
|
||||||
|
|
||||||
const emptyRow = {
|
const emptyRow = {
|
||||||
user: undefined,
|
user: undefined,
|
||||||
@@ -49,42 +49,24 @@ const emptyRow = {
|
|||||||
scheduled_for: undefined
|
scheduled_for: undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const formSchemaAssigned = formSchema.extend({
|
type Schema = z.infer<typeof formSchema>
|
||||||
coupon: z
|
|
||||||
.object({
|
|
||||||
code: z.string(),
|
|
||||||
type: z.enum(['FIXED', 'PERCENT']),
|
|
||||||
amount: z.number().positive()
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
})
|
|
||||||
|
|
||||||
type Schema = z.infer<typeof formSchemaAssigned>
|
|
||||||
|
|
||||||
type AssignedProps = {
|
type AssignedProps = {
|
||||||
onSubmit: (value: any) => void | Promise<void>
|
|
||||||
courses: Promise<{ hits: Course[] }>
|
courses: Promise<{ hits: Course[] }>
|
||||||
enrollments: Enrollment[]
|
|
||||||
coupon?: object
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Assigned({
|
export function Assigned({ courses }: AssignedProps) {
|
||||||
courses,
|
|
||||||
onSubmit,
|
|
||||||
enrollments,
|
|
||||||
coupon: couponInit
|
|
||||||
}: AssignedProps) {
|
|
||||||
const wizard = useWizard()
|
const wizard = useWizard()
|
||||||
const { orgid } = useParams()
|
const { orgid } = useParams()
|
||||||
|
const { update, ...state } = useWizardStore()
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
resolver: zodResolver(formSchemaAssigned),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
coupon: couponInit,
|
enrollments: state.enrollments.length
|
||||||
enrollments: enrollments.length
|
? state.enrollments.map((e: any) => ({
|
||||||
? enrollments.map((e: any) => ({
|
|
||||||
...e,
|
...e,
|
||||||
scheduled_for: e.scheduled_for
|
scheduled_for: e.scheduled_for
|
||||||
? new Date(e.scheduled_for)
|
? DateTime.fromISO(e.scheduled_for, { zone: 'local' }).toJSDate()
|
||||||
: undefined
|
: undefined
|
||||||
}))
|
}))
|
||||||
: [emptyRow]
|
: [emptyRow]
|
||||||
@@ -96,15 +78,10 @@ export function Assigned({
|
|||||||
control,
|
control,
|
||||||
name: 'enrollments'
|
name: 'enrollments'
|
||||||
})
|
})
|
||||||
const items = useWatch({
|
const enrollments = useWatch({
|
||||||
control,
|
control,
|
||||||
name: 'enrollments'
|
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 onSearch = async (search: string) => {
|
||||||
const params = new URLSearchParams({ q: search })
|
const params = new URLSearchParams({ q: search })
|
||||||
@@ -113,24 +90,22 @@ export function Assigned({
|
|||||||
return hits
|
return hits
|
||||||
}
|
}
|
||||||
|
|
||||||
const onSubmit_ = async ({ enrollments, coupon }: Schema) => {
|
const onSubmit = async ({ enrollments }: Schema) => {
|
||||||
const items = Object.values(
|
update({ enrollments })
|
||||||
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 })
|
|
||||||
wizard('payment')
|
wizard('payment')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const parsed = formSchema.safeParse({ enrollments })
|
||||||
|
|
||||||
|
if (parsed.success) {
|
||||||
|
update(parsed.data)
|
||||||
|
}
|
||||||
|
}, [enrollments])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<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]">
|
<div className="grid w-full gap-3 lg:grid-cols-[3fr_3fr_2fr_2fr_auto]">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<>
|
<>
|
||||||
@@ -166,7 +141,9 @@ export function Assigned({
|
|||||||
|
|
||||||
{/* Rows */}
|
{/* Rows */}
|
||||||
{fields.map((field, index) => {
|
{fields.map((field, index) => {
|
||||||
const { unit_price } = items?.[index]?.course || { unit_price: 0 }
|
const { unit_price } = enrollments?.[index]?.course || {
|
||||||
|
unit_price: 0
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment key={field.id}>
|
<Fragment key={field.id}>
|
||||||
@@ -281,7 +258,7 @@ export function Assigned({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Summary */}
|
{/* Summary */}
|
||||||
<Summary {...{ subtotal, coupon, setValue }} />
|
<Summary />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Fragment } from 'react'
|
import { Fragment, useEffect } from 'react'
|
||||||
import {
|
import {
|
||||||
ArrowRightIcon,
|
ArrowRightIcon,
|
||||||
MinusIcon,
|
MinusIcon,
|
||||||
@@ -36,9 +36,11 @@ import { MAX_ITEMS, type Course } from '../_.$orgid.enrollments.add/data'
|
|||||||
import { Discount, applyDiscount, type Coupon } from './discount'
|
import { Discount, applyDiscount, type Coupon } from './discount'
|
||||||
import { currency } from './utils'
|
import { currency } from './utils'
|
||||||
import { useWizard } from '@/components/wizard'
|
import { useWizard } from '@/components/wizard'
|
||||||
|
import { useWizardStore } from './store'
|
||||||
|
|
||||||
const emptyRow = {
|
const emptyRow = {
|
||||||
course: undefined
|
course: undefined as any,
|
||||||
|
quantity: 1
|
||||||
}
|
}
|
||||||
|
|
||||||
const item = z.object({
|
const item = z.object({
|
||||||
@@ -57,39 +59,24 @@ const item = z.object({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
items: z.array(item).min(1).max(MAX_ITEMS),
|
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()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
type Schema = z.infer<typeof formSchema>
|
type Schema = z.input<typeof formSchema>
|
||||||
|
|
||||||
export type Item = z.infer<typeof item>
|
export type Item = z.infer<typeof item>
|
||||||
|
|
||||||
type BulkProps = {
|
type BulkProps = {
|
||||||
onSubmit: (value: any) => void | Promise<void>
|
|
||||||
courses: Promise<{ hits: Course[] }>
|
courses: Promise<{ hits: Course[] }>
|
||||||
items: Item[]
|
|
||||||
coupon?: Coupon
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Bulk({
|
export function Bulk({ courses }: BulkProps) {
|
||||||
courses,
|
|
||||||
onSubmit,
|
|
||||||
items: itemsInit,
|
|
||||||
coupon: couponInit
|
|
||||||
}: BulkProps) {
|
|
||||||
const wizard = useWizard()
|
const wizard = useWizard()
|
||||||
|
const { update, ...state } = useWizardStore()
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
items: itemsInit?.length ? itemsInit : [emptyRow],
|
items: state.items.length ? state.items : [emptyRow]
|
||||||
coupon: couponInit
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const {
|
const {
|
||||||
@@ -109,35 +96,20 @@ export function Bulk({
|
|||||||
control,
|
control,
|
||||||
name: 'items'
|
name: 'items'
|
||||||
})
|
})
|
||||||
const coupon = useWatch({ control, name: 'coupon' })
|
|
||||||
|
|
||||||
const subtotal = items.reduce(
|
const onSubmit_ = async ({ items }: Schema) => {
|
||||||
(acc, { course, quantity }) =>
|
update({ items })
|
||||||
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: [] })
|
|
||||||
wizard('payment')
|
wizard('payment')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const parsed = formSchema.safeParse({ items })
|
||||||
|
|
||||||
|
if (parsed.success) {
|
||||||
|
update(parsed.data)
|
||||||
|
}
|
||||||
|
}, [items])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={handleSubmit(onSubmit_)} className="space-y-4">
|
<form onSubmit={handleSubmit(onSubmit_)} className="space-y-4">
|
||||||
@@ -316,7 +288,7 @@ export function Bulk({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Summary {...{ subtotal, coupon, setValue }} />
|
<Summary />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
@@ -337,17 +309,9 @@ export function Bulk({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
type SummaryProps = {
|
export function Summary() {
|
||||||
subtotal: number
|
const { summary, coupon, update } = useWizardStore()
|
||||||
coupon?: Coupon
|
const { total, discount, subtotal } = summary()
|
||||||
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
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -407,7 +371,7 @@ export function Summary({ subtotal, coupon, setValue }: SummaryProps) {
|
|||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setValue('coupon', null)
|
update({ coupon: undefined })
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<XIcon />
|
<XIcon />
|
||||||
@@ -416,7 +380,7 @@ export function Summary({ subtotal, coupon, setValue }: SummaryProps) {
|
|||||||
<Discount
|
<Discount
|
||||||
disabled={subtotal === 0}
|
disabled={subtotal === 0}
|
||||||
onChange={(coupon) => {
|
onChange={(coupon) => {
|
||||||
setValue('coupon', coupon)
|
update({ coupon })
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -72,8 +72,6 @@ export function Discount({ onChange, ...props }: DiscountProps) {
|
|||||||
discount_type: 'FIXED' | 'PERCENT'
|
discount_type: 'FIXED' | 'PERCENT'
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(code)
|
|
||||||
|
|
||||||
onChange?.({ code, amount, type })
|
onChange?.({ code, amount, type })
|
||||||
|
|
||||||
reset()
|
reset()
|
||||||
|
|||||||
@@ -34,8 +34,9 @@ import { Currency } from '@repo/ui/components/currency'
|
|||||||
import { useWizard } from '@/components/wizard'
|
import { useWizard } from '@/components/wizard'
|
||||||
import { isName } from '../_.$orgid.users.add/data'
|
import { isName } from '../_.$orgid.users.add/data'
|
||||||
import type { PaymentMethod } from '@repo/ui/routes/orders/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 { applyDiscount } from './discount'
|
||||||
|
import { useWizardStore } from './store'
|
||||||
|
|
||||||
const creditCard = z.object({
|
const creditCard = z.object({
|
||||||
holder_name: z
|
holder_name: z
|
||||||
@@ -55,49 +56,42 @@ const creditCard = z.object({
|
|||||||
cvv: z.string().min(3).max(4)
|
cvv: z.string().min(3).max(4)
|
||||||
})
|
})
|
||||||
|
|
||||||
const formSchema = z.discriminatedUnion('payment_method', [
|
const formSchema = z.discriminatedUnion(
|
||||||
z.object({
|
'payment_method',
|
||||||
payment_method: z.literal('PIX')
|
[
|
||||||
}),
|
z.object({
|
||||||
|
payment_method: z.literal('PIX')
|
||||||
|
}),
|
||||||
|
|
||||||
z.object({
|
z.object({
|
||||||
payment_method: z.literal('BANK_SLIP')
|
payment_method: z.literal('BANK_SLIP')
|
||||||
}),
|
}),
|
||||||
|
|
||||||
z.object({
|
z.object({
|
||||||
payment_method: z.literal('MANUAL')
|
payment_method: z.literal('MANUAL')
|
||||||
}),
|
}),
|
||||||
|
|
||||||
z.object({
|
z.object({
|
||||||
payment_method: z.literal('CREDIT_CARD'),
|
payment_method: z.literal('CREDIT_CARD'),
|
||||||
credit_card: creditCard,
|
credit_card: creditCard,
|
||||||
installments: z.coerce.number().int().min(1).max(12)
|
installments: z.coerce.number().int().min(1).max(12)
|
||||||
})
|
})
|
||||||
])
|
],
|
||||||
|
{ error: 'Escolha uma forma de pagamento' }
|
||||||
|
)
|
||||||
|
|
||||||
type Schema = z.input<typeof formSchema>
|
type Schema = z.input<typeof formSchema>
|
||||||
|
|
||||||
export type CreditCard = z.infer<typeof creditCard>
|
export type CreditCard = z.infer<typeof creditCard>
|
||||||
|
|
||||||
type PaymentProps = {
|
export function Payment({}) {
|
||||||
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) {
|
|
||||||
const wizard = useWizard()
|
const wizard = useWizard()
|
||||||
|
const { update, ...state } = useWizardStore()
|
||||||
const { control, handleSubmit } = useForm<Schema>({
|
const { control, handleSubmit } = useForm<Schema>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
payment_method: paymentMethodInit,
|
payment_method: state.payment_method,
|
||||||
installments: state?.installments ?? 1,
|
installments: state.installments,
|
||||||
credit_card: creditCardInit
|
credit_card: state.credit_card
|
||||||
},
|
},
|
||||||
resolver: zodResolver(formSchema)
|
resolver: zodResolver(formSchema)
|
||||||
})
|
})
|
||||||
@@ -114,13 +108,23 @@ export function Payment({
|
|||||||
: 0
|
: 0
|
||||||
const total = subtotal > 0 ? subtotal + discount : 0
|
const total = subtotal > 0 ? subtotal + discount : 0
|
||||||
|
|
||||||
const onSubmit_ = async (data: Schema) => {
|
const onSubmit = async ({ payment_method, ...data }: Schema) => {
|
||||||
await onSubmit({ credit_card: undefined, ...data })
|
if (payment_method === 'CREDIT_CARD') {
|
||||||
|
// @ts-ignore
|
||||||
|
update({ payment_method, ...data })
|
||||||
|
} else {
|
||||||
|
update({
|
||||||
|
payment_method,
|
||||||
|
installments: undefined,
|
||||||
|
credit_card: undefined
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
wizard('review')
|
wizard('review')
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(onSubmit_)} className="space-y-4">
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||||
<Controller
|
<Controller
|
||||||
name="payment_method"
|
name="payment_method"
|
||||||
control={control}
|
control={control}
|
||||||
@@ -206,6 +210,7 @@ export function CreditCard({
|
|||||||
{/* Credir card number */}
|
{/* Credir card number */}
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
name="credit_card.number"
|
name="credit_card.number"
|
||||||
render={({ field: { onChange, ref, ...field }, fieldState }) => (
|
render={({ field: { onChange, ref, ...field }, fieldState }) => (
|
||||||
<Field data-invalid={fieldState.invalid}>
|
<Field data-invalid={fieldState.invalid}>
|
||||||
|
|||||||
@@ -37,11 +37,11 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger
|
DialogTrigger
|
||||||
} from '@repo/ui/components/ui/dialog'
|
} from '@repo/ui/components/ui/dialog'
|
||||||
|
import { paymentMethods } from '@repo/ui/routes/orders/data'
|
||||||
|
|
||||||
import { useWizard } from '@/components/wizard'
|
import { useWizard } from '@/components/wizard'
|
||||||
import { type WizardState } from './route'
|
import { type WizardState } from './store'
|
||||||
import { applyDiscount } from './discount'
|
import { applyDiscount } from './discount'
|
||||||
import { paymentMethods } from '@repo/ui/routes/orders/data'
|
|
||||||
import {
|
import {
|
||||||
Field,
|
Field,
|
||||||
FieldDescription,
|
FieldDescription,
|
||||||
@@ -57,46 +57,28 @@ import {
|
|||||||
InputGroupButton,
|
InputGroupButton,
|
||||||
InputGroupInput
|
InputGroupInput
|
||||||
} from '@repo/ui/components/ui/input-group'
|
} from '@repo/ui/components/ui/input-group'
|
||||||
import { calcInterest } from './payment'
|
|
||||||
|
import { useWizardStore } from './store'
|
||||||
|
|
||||||
type ReviewProps = {
|
type ReviewProps = {
|
||||||
state: WizardState
|
|
||||||
onSubmit: (value: WizardState) => void | Promise<void>
|
onSubmit: (value: WizardState) => void | Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Review({ state, onSubmit }: ReviewProps) {
|
export function Review({ onSubmit }: ReviewProps) {
|
||||||
const wizard = useWizard()
|
const wizard = useWizard()
|
||||||
|
const { items, summary } = useWizardStore()
|
||||||
|
const { subtotal, discount, interest_amount, total } = summary()
|
||||||
const [loading, { set }] = useToggle()
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Address total={total} {...state} />
|
<Summary />
|
||||||
|
|
||||||
<form
|
<form
|
||||||
onSubmit={async (e) => {
|
onSubmit={async (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
set(true)
|
set(true)
|
||||||
await onSubmit(state)
|
// await onSubmit(state)
|
||||||
}}
|
}}
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
>
|
>
|
||||||
@@ -164,7 +146,7 @@ export function Review({ state, onSubmit }: ReviewProps) {
|
|||||||
Total
|
Total
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Currency>{total + interest_amount}</Currency>
|
<Currency>{total}</Currency>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableFooter>
|
</TableFooter>
|
||||||
@@ -193,11 +175,10 @@ export function Review({ state, onSubmit }: ReviewProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Address({
|
export function Summary() {
|
||||||
total,
|
const { summary, credit_card, payment_method, installments } =
|
||||||
payment_method,
|
useWizardStore()
|
||||||
credit_card
|
const { total } = summary()
|
||||||
}: WizardState & { total: number }) {
|
|
||||||
const numberValidation = valid.number(credit_card?.number)
|
const numberValidation = valid.number(credit_card?.number)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -229,7 +210,8 @@ export function Address({
|
|||||||
{numberValidation.card?.niceType} (Crédito) ****{' '}
|
{numberValidation.card?.niceType} (Crédito) ****{' '}
|
||||||
{credit_card.number.slice(-4)}
|
{credit_card.number.slice(-4)}
|
||||||
<br />
|
<br />
|
||||||
1x <Currency>{total}</Currency>
|
{installments}x{' '}
|
||||||
|
<Currency>{total / Number(installments)}</Currency>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { Route } from './+types/route'
|
import type { Route } from './+types/route'
|
||||||
|
|
||||||
import { useState } from 'react'
|
import { useFetcher } from 'react-router'
|
||||||
import { Link } 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 { BookSearchIcon, CircleCheckBigIcon, WalletIcon } from 'lucide-react'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -25,39 +25,17 @@ import { createSearch } from '@repo/util/meili'
|
|||||||
import { cloudflareContext } from '@repo/auth/context'
|
import { cloudflareContext } from '@repo/auth/context'
|
||||||
import { Label } from '@repo/ui/components/ui/label'
|
import { Label } from '@repo/ui/components/ui/label'
|
||||||
import { Skeleton } from '@repo/ui/components/skeleton'
|
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 { 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 { Assigned } from './assigned'
|
||||||
import { Bulk, type Item } from './bulk'
|
|
||||||
import { Payment, type CreditCard } from './payment'
|
|
||||||
import { Review } from './review'
|
import { Review } from './review'
|
||||||
import type { Coupon } from './discount'
|
|
||||||
import { useFetcher } from 'react-router'
|
|
||||||
|
|
||||||
export type WizardState = {
|
import { useWizardStore, type WizardState } from './store'
|
||||||
index: number
|
import { useState } from 'react'
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
export function meta({}: Route.MetaArgs) {
|
export function meta({}: Route.MetaArgs) {
|
||||||
return [{ title: 'Comprar matrículas' }]
|
return [{ title: 'Comprar matrículas' }]
|
||||||
@@ -87,20 +65,7 @@ export default function Route({
|
|||||||
}: Route.ComponentProps) {
|
}: Route.ComponentProps) {
|
||||||
const fetcher = useFetcher()
|
const fetcher = useFetcher()
|
||||||
const [mounted, setMounted] = useState(false)
|
const [mounted, setMounted] = useState(false)
|
||||||
const [state, setState] = useLocalStorageState<WizardState>('wizard_cart', {
|
const { index, kind, setIndex, setKind } = useWizardStore()
|
||||||
defaultValue: emptyWizard
|
|
||||||
})
|
|
||||||
const index = state.index
|
|
||||||
const kind = state.kind
|
|
||||||
const props = {
|
|
||||||
...state,
|
|
||||||
courses,
|
|
||||||
onSubmit: async (data: any) =>
|
|
||||||
setState((prev) => ({
|
|
||||||
...(prev ?? emptyWizard),
|
|
||||||
...data
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
const onSubmit = async (data: WizardState) => {
|
const onSubmit = async (data: WizardState) => {
|
||||||
await fetcher.submit(JSON.stringify(data), {
|
await fetcher.submit(JSON.stringify(data), {
|
||||||
@@ -157,15 +122,7 @@ export default function Route({
|
|||||||
</StepItem>
|
</StepItem>
|
||||||
</Step>
|
</Step>
|
||||||
|
|
||||||
<Wizard
|
<Wizard index={index} onChange={setIndex}>
|
||||||
index={index}
|
|
||||||
onChange={(nextIndex) =>
|
|
||||||
setState((prev) => ({
|
|
||||||
...(prev ?? emptyWizard),
|
|
||||||
index: nextIndex
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{/* Cart */}
|
{/* Cart */}
|
||||||
<WizardStep name="cart">
|
<WizardStep name="cart">
|
||||||
<Label
|
<Label
|
||||||
@@ -187,40 +144,27 @@ export default function Route({
|
|||||||
<Switch
|
<Switch
|
||||||
checked={kind === 'assigned'}
|
checked={kind === 'assigned'}
|
||||||
onCheckedChange={(checked) =>
|
onCheckedChange={(checked) =>
|
||||||
setState((prev) => ({
|
setKind(checked ? 'assigned' : 'bulk')
|
||||||
...(prev ?? emptyWizard),
|
|
||||||
kind: checked ? 'assigned' : 'bulk'
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
/>
|
/>
|
||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
{kind == 'assigned' ? (
|
{kind === 'assigned' ? (
|
||||||
<Assigned {...props} />
|
<Assigned courses={courses} />
|
||||||
) : (
|
) : (
|
||||||
<Bulk {...props} />
|
<Bulk courses={courses} />
|
||||||
)}
|
)}
|
||||||
</WizardStep>
|
</WizardStep>
|
||||||
|
|
||||||
{/* Payment */}
|
{/* Payment */}
|
||||||
<WizardStep name="payment">
|
<WizardStep name="payment">
|
||||||
<Payment
|
<Payment />
|
||||||
state={state}
|
|
||||||
payment_method={state.payment_method}
|
|
||||||
credit_card={state.credit_card}
|
|
||||||
onSubmit={(data: any) => {
|
|
||||||
setState((prev) => ({
|
|
||||||
...(prev ?? emptyWizard),
|
|
||||||
...data
|
|
||||||
}))
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</WizardStep>
|
</WizardStep>
|
||||||
|
|
||||||
{/* Review */}
|
{/* Review */}
|
||||||
<WizardStep name="review">
|
<WizardStep name="review">
|
||||||
<Review state={state} onSubmit={onSubmit} />
|
<Review onSubmit={onSubmit} />
|
||||||
</WizardStep>
|
</WizardStep>
|
||||||
</Wizard>
|
</Wizard>
|
||||||
</CardContent>
|
</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'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
UserIcon
|
UserIcon
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { DateTime } from 'luxon'
|
import { DateTime as LuxonDateTime } from 'luxon'
|
||||||
import { Fragment, Suspense } from 'react'
|
import { Fragment, Suspense } from 'react'
|
||||||
import { Await } from 'react-router'
|
import { Await } from 'react-router'
|
||||||
|
|
||||||
@@ -67,6 +67,7 @@ import {
|
|||||||
import { Spinner } from '@repo/ui/components/ui/spinner'
|
import { Spinner } from '@repo/ui/components/ui/spinner'
|
||||||
import { useParams } from 'react-router'
|
import { useParams } from 'react-router'
|
||||||
import { useRevalidator } from 'react-router'
|
import { useRevalidator } from 'react-router'
|
||||||
|
import { DateTime } from '@repo/ui/components/datetime'
|
||||||
import {
|
import {
|
||||||
Tabs,
|
Tabs,
|
||||||
TabsContent,
|
TabsContent,
|
||||||
@@ -239,7 +240,7 @@ function Timeline({
|
|||||||
{events.map(([run_at, items], index) => (
|
{events.map(([run_at, items], index) => (
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-5 gap-2.5" key={index}>
|
<div className="grid grid-cols-1 lg:grid-cols-5 gap-2.5" key={index}>
|
||||||
<div>
|
<div>
|
||||||
{DateTime.fromISO(run_at)
|
{LuxonDateTime.fromISO(run_at)
|
||||||
.setLocale('pt-BR')
|
.setLocale('pt-BR')
|
||||||
.toFormat('cccc, dd LLL yyyy')}
|
.toFormat('cccc, dd LLL yyyy')}
|
||||||
</div>
|
</div>
|
||||||
@@ -276,7 +277,13 @@ function Scheduled({ items = [] }) {
|
|||||||
<ul className="lg:flex gap-2.5 text-muted-foreground text-sm *:flex *:gap-1 *:items-center">
|
<ul className="lg:flex gap-2.5 text-muted-foreground text-sm *:flex *:gap-1 *:items-center">
|
||||||
<li>
|
<li>
|
||||||
<CalendarIcon className="size-3.5" />
|
<CalendarIcon className="size-3.5" />
|
||||||
<span>{datetime.format(new Date(scheduled_at))}</span>
|
<span>
|
||||||
|
<DateTime
|
||||||
|
options={{ hour: '2-digit', minute: '2-digit' }}
|
||||||
|
>
|
||||||
|
{scheduled_at}
|
||||||
|
</DateTime>
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<UserIcon className="size-3.5" />
|
<UserIcon className="size-3.5" />
|
||||||
@@ -345,7 +352,13 @@ function Executed({ items = [] }) {
|
|||||||
<ul className="lg:flex gap-2.5 text-muted-foreground text-sm *:flex *:gap-1 *:items-center">
|
<ul className="lg:flex gap-2.5 text-muted-foreground text-sm *:flex *:gap-1 *:items-center">
|
||||||
<li>
|
<li>
|
||||||
<ClockCheckIcon className="size-3.5" />
|
<ClockCheckIcon className="size-3.5" />
|
||||||
<span>{datetime.format(new Date(created_at))}</span>
|
<span>
|
||||||
|
<DateTime
|
||||||
|
options={{ hour: '2-digit', minute: '2-digit' }}
|
||||||
|
>
|
||||||
|
{created_at}
|
||||||
|
</DateTime>
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -384,7 +397,13 @@ function Failed({ items = [] }) {
|
|||||||
<ul className="lg:flex gap-2.5 text-muted-foreground text-sm *:flex *:gap-1 *:items-center">
|
<ul className="lg:flex gap-2.5 text-muted-foreground text-sm *:flex *:gap-1 *:items-center">
|
||||||
<li>
|
<li>
|
||||||
<ClockAlertIcon className="size-3.5" />
|
<ClockAlertIcon className="size-3.5" />
|
||||||
<span>{datetime.format(new Date(created_at))}</span>
|
<span>
|
||||||
|
<DateTime
|
||||||
|
options={{ hour: '2-digit', minute: '2-digit' }}
|
||||||
|
>
|
||||||
|
{created_at}
|
||||||
|
</DateTime>
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
{cause?.type === 'DeduplicationConflictError' ? (
|
{cause?.type === 'DeduplicationConflictError' ? (
|
||||||
@@ -535,11 +554,3 @@ function grouping(items) {
|
|||||||
)
|
)
|
||||||
return newItems.sort((x, y) => x[0].localeCompare(y[0]))
|
return newItems.sort((x, y) => x[0].localeCompare(y[0]))
|
||||||
}
|
}
|
||||||
|
|
||||||
const datetime = new Intl.DateTimeFormat('pt-BR', {
|
|
||||||
day: '2-digit',
|
|
||||||
month: '2-digit',
|
|
||||||
year: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -31,7 +31,8 @@
|
|||||||
"react-dom": "^19.2.1",
|
"react-dom": "^19.2.1",
|
||||||
"react-router": "^7.10.1",
|
"react-router": "^7.10.1",
|
||||||
"unique-names-generator": "^4.7.1",
|
"unique-names-generator": "^4.7.1",
|
||||||
"zod": "^4.1.13"
|
"zod": "^4.1.13",
|
||||||
|
"zustand": "^5.0.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cloudflare/vite-plugin": "^1.17.0",
|
"@cloudflare/vite-plugin": "^1.17.0",
|
||||||
|
|||||||
32
package-lock.json
generated
32
package-lock.json
generated
@@ -44,7 +44,8 @@
|
|||||||
"react-dom": "^19.2.1",
|
"react-dom": "^19.2.1",
|
||||||
"react-router": "^7.10.1",
|
"react-router": "^7.10.1",
|
||||||
"unique-names-generator": "^4.7.1",
|
"unique-names-generator": "^4.7.1",
|
||||||
"zod": "^4.1.13"
|
"zod": "^4.1.13",
|
||||||
|
"zustand": "^5.0.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cloudflare/vite-plugin": "^1.17.0",
|
"@cloudflare/vite-plugin": "^1.17.0",
|
||||||
@@ -7488,6 +7489,35 @@
|
|||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/zustand": {
|
||||||
|
"version": "5.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.9.tgz",
|
||||||
|
"integrity": "sha512-ALBtUj0AfjJt3uNRQoL1tL2tMvj6Gp/6e39dnfT6uzpelGru8v1tPOGBzayOWbPJvujM8JojDk3E1LxeFisBNg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.20.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": ">=18.0.0",
|
||||||
|
"immer": ">=9.0.6",
|
||||||
|
"react": ">=18.0.0",
|
||||||
|
"use-sync-external-store": ">=1.2.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"immer": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"use-sync-external-store": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"packages/auth": {
|
"packages/auth": {
|
||||||
"name": "@repo/auth",
|
"name": "@repo/auth",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user