From bad7e15f46abd39213352d47cf8cdf91b0f1743f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Rafael=20Siqueira?= Date: Tue, 30 Dec 2025 14:37:53 -0300 Subject: [PATCH] add zustard --- api.saladeaula.digital/app/app.py | 1 + .../app/routes/orgs/__init__.py | 2 + .../tests/routes/orgs/test_address.py | 22 +++ .../routes/_.$orgid.billing._index/data.ts | 2 +- .../route.tsx | 16 +-- .../_.$orgid.enrollments.buy/assigned.tsx | 75 ++++------ .../routes/_.$orgid.enrollments.buy/bulk.tsx | 86 ++++------- .../_.$orgid.enrollments.buy/discount.tsx | 2 - .../_.$orgid.enrollments.buy/payment.tsx | 77 +++++----- .../_.$orgid.enrollments.buy/review.tsx | 50 +++---- .../routes/_.$orgid.enrollments.buy/route.tsx | 88 +++--------- .../routes/_.$orgid.enrollments.buy/store.tsx | 136 ++++++++++++++++++ .../app/routes/_.$orgid.scheduled/route.tsx | 37 +++-- apps/admin.saladeaula.digital/package.json | 3 +- package-lock.json | 32 ++++- 15 files changed, 349 insertions(+), 280 deletions(-) create mode 100644 api.saladeaula.digital/tests/routes/orgs/test_address.py create mode 100644 apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/store.tsx diff --git a/api.saladeaula.digital/app/app.py b/api.saladeaula.digital/app/app.py index 28ad610..ca62fa4 100644 --- a/api.saladeaula.digital/app/app.py +++ b/api.saladeaula.digital/app/app.py @@ -50,6 +50,7 @@ app.include_router(users.password, prefix='/users') app.include_router(orders.router, prefix='/orders') app.include_router(orders.checkout, prefix='/orders') 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.billing, prefix='/orgs') app.include_router(orgs.custom_pricing, prefix='/orgs') diff --git a/api.saladeaula.digital/app/routes/orgs/__init__.py b/api.saladeaula.digital/app/routes/orgs/__init__.py index f153e36..7da4daf 100644 --- a/api.saladeaula.digital/app/routes/orgs/__init__.py +++ b/api.saladeaula.digital/app/routes/orgs/__init__.py @@ -1,4 +1,5 @@ from .add import router as add +from .address import router as address from .admins import router as admins from .billing import router as billing from .custom_pricing import router as custom_pricing @@ -9,6 +10,7 @@ from .users.batch_jobs import router as batch_jobs __all__ = [ 'add', + 'address', 'admins', 'billing', 'custom_pricing', diff --git a/api.saladeaula.digital/tests/routes/orgs/test_address.py b/api.saladeaula.digital/tests/routes/orgs/test_address.py new file mode 100644 index 0000000..4d9ddcc --- /dev/null +++ b/api.saladeaula.digital/tests/routes/orgs/test_address.py @@ -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 diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.billing._index/data.ts b/apps/admin.saladeaula.digital/app/routes/_.$orgid.billing._index/data.ts index 59dc1e0..1531207 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.billing._index/data.ts +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.billing._index/data.ts @@ -20,4 +20,4 @@ export const labels: Record = { CLOSED: 'Fechado' } -export const tz = 'America/Sao_Paulo' +export const tz = 'local' diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.$id.submitted/route.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.$id.submitted/route.tsx index 5ef0897..81901b6 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.$id.submitted/route.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.$id.submitted/route.tsx @@ -203,7 +203,7 @@ export default function Route({ {LuxonDateTime.fromISO( output.scheduled_for, - { zone: 'America/Sao_Paulo' } + { zone: 'local' } ).toJSDate()} {} @@ -239,7 +239,11 @@ export default function Route({ >
  • - {datetime.format(new Date(sk))} + + {sk} +
  • {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() { return ( <> diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/assigned.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/assigned.tsx index 7e1a68d..4529b96 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/assigned.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/assigned.tsx @@ -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 +type Schema = z.infer type AssignedProps = { - onSubmit: (value: any) => void | Promise 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>((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 (
    - +
    {/* 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 ( @@ -281,7 +258,7 @@ export function Assigned({
    {/* Summary */} - + diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/bulk.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/bulk.tsx index 6ff09b8..d378c94 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/bulk.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/bulk.tsx @@ -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 +type Schema = z.input export type Item = z.infer type BulkProps = { - onSubmit: (value: any) => void | Promise 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>((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 ( @@ -316,7 +288,7 @@ export function Bulk({ - + @@ -337,17 +309,9 @@ export function Bulk({ ) } -type SummaryProps = { - subtotal: number - coupon?: Coupon - setValue: UseFormSetValue -} - -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 }) }} > @@ -416,7 +380,7 @@ export function Summary({ subtotal, coupon, setValue }: SummaryProps) { { - setValue('coupon', coupon) + update({ coupon }) }} /> )} diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/discount.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/discount.tsx index 2ea8935..7d99a74 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/discount.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/discount.tsx @@ -72,8 +72,6 @@ export function Discount({ onChange, ...props }: DiscountProps) { discount_type: 'FIXED' | 'PERCENT' } - console.log(code) - onChange?.({ code, amount, type }) reset() diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/payment.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/payment.tsx index 409fc06..b506d52 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/payment.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/payment.tsx @@ -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 export type CreditCard = z.infer -type PaymentProps = { - state: WizardState - onSubmit: (value: any) => void | Promise - 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({ 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 ( - + ( diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/review.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/review.tsx index ced263e..f563a78 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/review.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/review.tsx @@ -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 } -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 ( <> -
    + { 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 - {total + interest_amount} + {total} @@ -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)}
    - 1x {total} + {installments}x{' '} + {total / Number(installments)} ) : ( <> diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/route.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/route.tsx index 494fdee..a3b42f2 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/route.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/route.tsx @@ -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('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({ - - setState((prev) => ({ - ...(prev ?? emptyWizard), - index: nextIndex - })) - } - > + {/* Cart */} - {kind == 'assigned' ? ( - + {kind === 'assigned' ? ( + ) : ( - + )} {/* Payment */} - { - setState((prev) => ({ - ...(prev ?? emptyWizard), - ...data - })) - }} - /> + {/* Review */} - + diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/store.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/store.tsx new file mode 100644 index 0000000..210475e --- /dev/null +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.buy/store.tsx @@ -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) => void + reset: () => void + summary: () => Summary +} + +const emptyWizard: WizardState = { + index: 0, + kind: 'bulk', + items: [], + enrollments: [] +} + +export const useWizardStore = create()( + 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>( + (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>((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' + } + ) +) diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.scheduled/route.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.scheduled/route.tsx index 13b2985..f5495b4 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.scheduled/route.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.scheduled/route.tsx @@ -16,7 +16,7 @@ import { UserIcon } from 'lucide-react' import { toast } from 'sonner' -import { DateTime } from 'luxon' +import { DateTime as LuxonDateTime } from 'luxon' import { Fragment, Suspense } from 'react' import { Await } from 'react-router' @@ -67,6 +67,7 @@ import { import { Spinner } from '@repo/ui/components/ui/spinner' import { useParams } from 'react-router' import { useRevalidator } from 'react-router' +import { DateTime } from '@repo/ui/components/datetime' import { Tabs, TabsContent, @@ -239,7 +240,7 @@ function Timeline({ {events.map(([run_at, items], index) => (
    - {DateTime.fromISO(run_at) + {LuxonDateTime.fromISO(run_at) .setLocale('pt-BR') .toFormat('cccc, dd LLL yyyy')}
    @@ -276,7 +277,13 @@ function Scheduled({ items = [] }) {
    • - {datetime.format(new Date(scheduled_at))} + + + {scheduled_at} + +
    • @@ -345,7 +352,13 @@ function Executed({ items = [] }) {
      • - {datetime.format(new Date(created_at))} + + + {created_at} + +
    @@ -384,7 +397,13 @@ function Failed({ items = [] }) {
    • - {datetime.format(new Date(created_at))} + + + {created_at} + +
    • {cause?.type === 'DeduplicationConflictError' ? ( @@ -535,11 +554,3 @@ function grouping(items) { ) 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' -}) diff --git a/apps/admin.saladeaula.digital/package.json b/apps/admin.saladeaula.digital/package.json index 3a35073..ef0a23f 100644 --- a/apps/admin.saladeaula.digital/package.json +++ b/apps/admin.saladeaula.digital/package.json @@ -31,7 +31,8 @@ "react-dom": "^19.2.1", "react-router": "^7.10.1", "unique-names-generator": "^4.7.1", - "zod": "^4.1.13" + "zod": "^4.1.13", + "zustand": "^5.0.9" }, "devDependencies": { "@cloudflare/vite-plugin": "^1.17.0", diff --git a/package-lock.json b/package-lock.json index fd69985..f3277d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,7 +44,8 @@ "react-dom": "^19.2.1", "react-router": "^7.10.1", "unique-names-generator": "^4.7.1", - "zod": "^4.1.13" + "zod": "^4.1.13", + "zustand": "^5.0.9" }, "devDependencies": { "@cloudflare/vite-plugin": "^1.17.0", @@ -7488,6 +7489,35 @@ "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": { "name": "@repo/auth", "version": "0.0.0",