reditect to order

This commit is contained in:
2026-01-14 21:08:23 -03:00
parent d893114e38
commit a3e4fe887d
22 changed files with 1105 additions and 1157 deletions

View File

@@ -1,11 +1,11 @@
import { BellIcon } from 'lucide-react'
import { Button } from '@repo/ui/components/ui/button'
import {
Popover,
PopoverContent,
PopoverTrigger
} from '@repo/ui/components/ui/popover'
import { Button } from '@repo/ui/components/ui/button'
export function Notification() {
return (

View File

@@ -1,5 +1,5 @@
import { type ReactNode, createContext, useContext } from 'react'
import { type LucideIcon } from 'lucide-react'
import { createContext, useContext, type ReactNode } from 'react'
import { cn } from '@repo/ui/lib/utils'

View File

@@ -4,8 +4,8 @@ import React, {
useContext,
useMemo,
useState,
type ReactNode,
type ReactElement
type ReactElement,
type ReactNode
} from 'react'
type WizardContextProps = (name: string) => void

View File

@@ -1,13 +1,13 @@
'use client'
import { formatCNPJ } from '@brazilian-utils/brazilian-utils'
import { IconRosetteDiscountCheckFilled } from '@tabler/icons-react'
import {
CheckIcon,
BadgeCheckIcon,
CheckIcon,
ChevronsUpDownIcon,
PlusIcon
} from 'lucide-react'
import { IconRosetteDiscountCheckFilled } from '@tabler/icons-react'
import { createContext, use } from 'react'
import { useLocation } from 'react-router'

View File

@@ -1,4 +1,4 @@
import { type LoaderFunctionArgs, createContext } from 'react-router'
import { createContext, type LoaderFunctionArgs } from 'react-router'
import { userContext } from '@repo/auth/context'
import { request as req } from '@repo/util/request'

View File

@@ -1,15 +1,15 @@
import { useSearchParams } from 'react-router'
import { ChevronRightIcon, ChevronLeftIcon } from 'lucide-react'
import { subMonths, addMonths } from 'date-fns'
import { addMonths, subMonths } from 'date-fns'
import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react'
import { DateTime as LuxonDateTime } from 'luxon'
import { useSearchParams } from 'react-router'
import { TZ } from '@/conf'
import { DateTime } from '@repo/ui/components/datetime'
import { Badge } from '@repo/ui/components/ui/badge'
import { Button } from '@repo/ui/components/ui/button'
import { ButtonGroup } from '@repo/ui/components/ui/button-group'
import { Badge } from '@repo/ui/components/ui/badge'
import { DateTime } from '@repo/ui/components/datetime'
import { formatDate, billingPeriod } from './util'
import { billingPeriod, formatDate } from './util'
type RangePeriodProps = {
startDate: Date

View File

@@ -1,15 +1,26 @@
import type { Route } from './+types/route'
import Fuse from 'fuse.js'
import { BanIcon } from 'lucide-react'
import { DateTime as LuxonDateTime } from 'luxon'
import { Suspense, useMemo } from 'react'
import { BanIcon } from 'lucide-react'
import { Await, useSearchParams } from 'react-router'
import { cn } from '@repo/ui/lib/utils'
import { request as req } from '@repo/util/request'
import { Abbr } from '@repo/ui/components/abbr'
import { Currency } from '@repo/ui/components/currency'
import { DateTime } from '@repo/ui/components/datetime'
import { SearchForm } from '@repo/ui/components/search-form'
import { Skeleton } from '@repo/ui/components/skeleton'
import { Button } from '@repo/ui/components/ui/button'
import { Card, CardContent } from '@repo/ui/components/ui/card'
import {
Empty,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle
} from '@repo/ui/components/ui/empty'
import { Kbd } from '@repo/ui/components/ui/kbd'
import {
Table,
TableBody,
@@ -19,24 +30,13 @@ import {
TableHeader,
TableRow
} from '@repo/ui/components/ui/table'
import { Abbr } from '@repo/ui/components/abbr'
import { Button } from '@repo/ui/components/ui/button'
import { SearchForm } from '@repo/ui/components/search-form'
import {
Empty,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle
} from '@repo/ui/components/ui/empty'
import { Kbd } from '@repo/ui/components/ui/kbd'
import { Currency } from '@repo/ui/components/currency'
import { DateTime } from '@repo/ui/components/datetime'
import { cn } from '@repo/ui/lib/utils'
import { request as req } from '@repo/util/request'
import { TZ } from '@/conf'
import { billingPeriod, formatDate } from './util'
import { RangePeriod } from './range-period'
import { statuses } from './data'
import { RangePeriod } from './range-period'
import { billingPeriod, formatDate } from './util'
export function meta({}) {
return [{ title: 'Resumo de cobranças' }]

View File

@@ -2,27 +2,36 @@ import type { Route } from './+types/route'
import {
AlertCircleIcon,
BanIcon,
CalendarIcon,
CheckCircle2Icon,
ClockIcon,
CalendarIcon,
UserIcon,
BanIcon,
PlusIcon,
EllipsisIcon,
RotateCcw
PlusIcon,
RotateCcw,
UserIcon
} from 'lucide-react'
import { Link, useParams } from 'react-router'
import { Suspense } from 'react'
import { DateTime as LuxonDateTime } from 'luxon'
import { Suspense } from 'react'
import { Link, useParams } from 'react-router'
import { Abbr } from '@repo/ui/components/abbr'
import { DateTime } from '@repo/ui/components/datetime'
import { Skeleton } from '@repo/ui/components/skeleton'
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle
} from '@repo/ui/components/ui/empty'
Alert,
AlertDescription,
AlertTitle
} from '@repo/ui/components/ui/alert'
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator
} from '@repo/ui/components/ui/breadcrumb'
import { Button } from '@repo/ui/components/ui/button'
import {
Card,
CardContent,
@@ -31,31 +40,22 @@ import {
CardHeader,
CardTitle
} from '@repo/ui/components/ui/card'
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator
} from '@repo/ui/components/ui/breadcrumb'
import {
Alert,
AlertDescription,
AlertTitle
} from '@repo/ui/components/ui/alert'
import { request as req } from '@repo/util/request'
import { Skeleton } from '@repo/ui/components/skeleton'
import { Await } from 'react-router'
import { Abbr } from '@repo/ui/components/abbr'
import { Button } from '@repo/ui/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from '@repo/ui/components/ui/dropdown-menu'
import { DateTime } from '@repo/ui/components/datetime'
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle
} from '@repo/ui/components/ui/empty'
import { request as req } from '@repo/util/request'
import { Await } from 'react-router'
export function meta({}: Route.MetaArgs) {
return [{ title: 'Relatório de matrículas' }]
@@ -69,9 +69,7 @@ export async function loader({ context, request, params }: Route.LoaderArgs) {
request
}).then((r) => r.json())
return {
submission
}
return { submission }
}
export default function Route({

View File

@@ -1,17 +1,12 @@
import type { Route } from './+types/route'
import { useEffect, useState } from 'react'
import { useFetcher, Link } from 'react-router'
import { useMount } from 'ahooks'
import { BookSearchIcon, CircleCheckBigIcon, WalletIcon } from 'lucide-react'
import { use, useEffect, useState } from 'react'
import { Link, redirect, useFetcher } from 'react-router'
import {
Card,
CardContent,
CardHeader,
CardDescription,
CardTitle
} from '@repo/ui/components/ui/card'
import { cloudflareContext, userContext } from '@repo/auth/context'
import { Skeleton } from '@repo/ui/components/skeleton'
import {
Breadcrumb,
BreadcrumbItem,
@@ -20,22 +15,27 @@ import {
BreadcrumbPage,
BreadcrumbSeparator
} from '@repo/ui/components/ui/breadcrumb'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle
} from '@repo/ui/components/ui/card'
import { Label } from '@repo/ui/components/ui/label'
import { Switch } from '@repo/ui/components/ui/switch'
import { createSearch } from '@repo/util/meili'
import { cloudflareContext, userContext } from '@repo/auth/context'
import { Label } from '@repo/ui/components/ui/label'
import { Skeleton } from '@repo/ui/components/skeleton'
import { request as req, HttpMethod } from '@repo/util/request'
import { HttpMethod, request as req } from '@repo/util/request'
import { INTERNAL_EMAIL_DOMAIN } from '@/conf'
import { workspaceContext } from '@/middleware/workspace'
import { useWorksapce } from '@/components/workspace-switcher'
import { Step, StepItem, StepSeparator } from '@/components/step'
import { Wizard, WizardStep } from '@/components/wizard'
import { useWorksapce } from '@/components/workspace-switcher'
import { INTERNAL_EMAIL_DOMAIN } from '@/conf'
import { workspaceContext } from '@/middleware/workspace'
import type { Course } from '../_.$orgid.enrollments.add/data'
import { Assigned } from './assigned'
import { Bulk } from './bulk'
import { Payment } from './payment'
import { Assigned } from './assigned'
import { Review } from './review'
import { useWizardStore } from './store'
@@ -43,7 +43,7 @@ export function meta({}: Route.MetaArgs) {
return [{ title: 'Comprar matrículas' }]
}
export async function loader({ context }: Route.LoaderArgs) {
export async function loader({ context, params, request }: Route.LoaderArgs) {
const cloudflare = context.get(cloudflareContext)
const courses = createSearch<Course>({
index: 'saladeaula_courses',
@@ -53,10 +53,16 @@ export async function loader({ context }: Route.LoaderArgs) {
env: cloudflare.env
})
return { courses }
const seats = req({
url: `/orgs/${params.orgid}/seats`,
request,
context
}).then((r) => r.json() as any)
return { courses, seats }
}
export async function action({ params, request, context }: Route.ActionArgs) {
export async function action({ request, context }: Route.ActionArgs) {
const body = (await request.json()) as object
const user = context.get(userContext)!
const { activeWorkspace } = context.get(workspaceContext)
@@ -83,13 +89,12 @@ export async function action({ params, request, context }: Route.ActionArgs) {
return { ok: false, error }
}
console.log(await r.json())
return { ok: true }
const { id } = (await r.json()) as { id: string }
throw redirect(`../payments/${id}`)
}
export default function Route({
loaderData: { courses }
loaderData: { courses, seats: seats_ }
}: Route.ComponentProps) {
const fetcher = useFetcher()
const [mounted, setMounted] = useState(false)
@@ -97,6 +102,9 @@ export default function Route({
const { index, kind, setIndex, setKind, reset, update, ...state } =
useWizardStore()
// @TODO
const seats = use(seats_)
const onSubmit = async () => {
const items = state.items.map(({ course, quantity }) => ({
...course,
@@ -119,14 +127,12 @@ export default function Route({
}
}, [address])
useEffect(() => {
console.log(fetcher.data)
}, [fetcher.data])
if (!mounted) {
return <Skeleton />
}
console.log(seats)
return (
<div className="space-y-2.5">
<Breadcrumb>

View File

@@ -1,8 +1,11 @@
import type { Route } from './+types/route'
import { Link } from 'react-router'
import { formatCEP } from '@brazilian-utils/brazilian-utils'
import { Suspense, useEffect } from 'react'
import { Await, Link } from 'react-router'
import { request as req } from '@repo/util/request'
import { Currency } from '@repo/ui/components/currency'
import { Skeleton } from '@repo/ui/components/skeleton'
import {
Breadcrumb,
BreadcrumbItem,
@@ -11,6 +14,22 @@ import {
BreadcrumbPage,
BreadcrumbSeparator
} from '@repo/ui/components/ui/breadcrumb'
import {
Card,
CardContent,
CardHeader,
CardTitle
} from '@repo/ui/components/ui/card'
import {
Item,
ItemContent,
ItemDescription,
ItemGroup,
ItemTitle
} from '@repo/ui/components/ui/item'
import { paymentMethods } from '@repo/ui/routes/orders/data'
import { request as req } from '@repo/util/request'
import { useWizardStore } from '../_.$orgid.enrollments.buy/store'
export function meta() {
return [
@@ -20,28 +39,32 @@ export function meta() {
]
}
export async function loader({ params, request, context }: Route.LoaderArgs) {
const r = await req({
export async function loader({ context, request, params }: Route.LoaderArgs) {
const order = await req({
url: `/orders/${params.id}`,
request,
context
})
context,
request
}).then((r) => r.json())
if (!r.ok) {
throw new Response(null, { status: r.status })
}
return { order: await r.json() }
return { order }
}
export default function Route({ loaderData: { order } }: Route.ComponentProps) {
const { reset } = useWizardStore()
const { address, total, credit_card, payment_method, invoice, installments } =
order
useEffect(() => {
reset()
}, [])
return (
<div className="space-y-2.5">
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link to="../payments">Histórico de pagamentos</Link>
<Link to="../payments">Pagamentos</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
@@ -50,7 +73,55 @@ export default function Route({ loaderData: { order } }: Route.ComponentProps) {
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<pre>{JSON.stringify(order, null, 2)}</pre>
<Card className="lg:max-w-4xl mx-auto space-y-2.5">
<CardHeader>
<CardTitle className="text-2xl">Detalhes do pagamento</CardTitle>
</CardHeader>
<CardContent>
<ItemGroup className="grid lg:grid-cols-2 gap-4">
<Item variant="outline">
<ItemContent>
<ItemTitle>Endereço de cobrança</ItemTitle>
<ul className="text-muted-foreground text-sm leading-normal font-normal text-balance">
{address?.address1}
{address?.address2 ? <>, {address?.address2}</> : null}
<br />
{address?.neighborhood}
<br />
{address?.city}, {address?.state}
<br />
{formatCEP(address?.postcode)}
</ul>
</ItemContent>
</Item>
<Item variant="outline" className="items-start">
<ItemContent>
<ItemTitle>Forma de pagamento</ItemTitle>
<ItemDescription>
{credit_card ? (
<>
{credit_card.brand} (Crédito) **** {credit_card.last4}
<br />
{installments}x{' '}
<Currency>{total / Number(installments)}</Currency>
</>
) : (
<>
{payment_method
? paymentMethods[payment_method]
: payment_method}
</>
)}
</ItemDescription>
</ItemContent>
</Item>
</ItemGroup>
{/*<pre>{JSON.stringify(order, null, 2)}</pre>*/}
</CardContent>
</Card>
</div>
)
}

View File

@@ -12,7 +12,7 @@
"typecheck": "npm run cf-typegen && react-router typegen && tsc -b"
},
"dependencies": {
"@react-router/fs-routes": "^7.10.1",
"@react-router/fs-routes": "^7.12.0",
"@repo/auth": "*",
"@repo/ui": "*",
"@repo/util": "^0.0.0",
@@ -24,31 +24,31 @@
"fuse.js": "^7.1.0",
"isbot": "^5.1.32",
"luxon": "^3.7.2",
"meilisearch": "^0.54.0",
"meilisearch": "^0.55.0",
"meilisearch-helper": "github:sergiors/meilisearch-helper",
"ramda": "^0.32.0",
"react": "^19.2.1",
"react-dom": "^19.2.1",
"react-router": "^7.10.1",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-router": "^7.12.0",
"unique-names-generator": "^4.7.1",
"zod": "^4.1.13",
"zustand": "^5.0.9"
"zod": "^4.3.5",
"zustand": "^5.0.10"
},
"devDependencies": {
"@cloudflare/vite-plugin": "^1.17.0",
"@tailwindcss/vite": "^4.1.17",
"@cloudflare/vite-plugin": "^1.20.3",
"@tailwindcss/vite": "^4.1.18",
"@types/file-saver": "^2.0.7",
"@types/luxon": "^3.7.1",
"@types/node": "^24.10.1",
"@types/react": "^19.2.7",
"@types/node": "^25.0.8",
"@types/react": "^19.2.8",
"@types/react-dom": "^19.2.3",
"prettier": "^3.7.4",
"remix-flat-routes": "^0.8.5",
"tailwindcss": "^4.1.17",
"tailwindcss": "^4.1.18",
"tw-animate-css": "^1.4.0",
"typescript": "^5.9.3",
"vite": "^7.2.6",
"vite-tsconfig-paths": "^5.1.4",
"wrangler": "^4.53.0"
"vite": "^7.3.1",
"vite-tsconfig-paths": "^6.0.4",
"wrangler": "^4.59.1"
}
}

View File

@@ -4,6 +4,7 @@ export default {
ssr: true,
future: {
v8_viteEnvironmentApi: true,
v8_splitRouteModules: true,
v8_middleware: true
}
} satisfies Config

View File

@@ -1,6 +1,6 @@
/* eslint-disable */
// Generated by Wrangler by running `wrangler types` (hash: 58c6149f238624e8945df7f87e575516)
// Runtime types generated with workerd@1.20251202.0 2025-04-04
// Runtime types generated with workerd@1.20260111.0 2025-04-04
declare namespace Cloudflare {
interface GlobalProps {
mainModule: typeof import("./workers/app");
@@ -500,8 +500,10 @@ interface DurableObjectNamespaceNewUniqueIdOptions {
jurisdiction?: DurableObjectJurisdiction;
}
type DurableObjectLocationHint = "wnam" | "enam" | "sam" | "weur" | "eeur" | "apac" | "oc" | "afr" | "me";
type DurableObjectRoutingMode = "primary-only";
interface DurableObjectNamespaceGetDurableObjectOptions {
locationHint?: DurableObjectLocationHint;
routingMode?: DurableObjectRoutingMode;
}
interface DurableObjectClass<_T extends Rpc.DurableObjectBranded | undefined = undefined> {
}
@@ -2085,6 +2087,8 @@ interface Transformer<I = any, O = any> {
expectedLength?: number;
}
interface StreamPipeOptions {
preventAbort?: boolean;
preventCancel?: boolean;
/**
* Pipes this readable stream to a given writable stream destination. The way in which the piping process behaves under various error conditions can be customized with a number of passed options. It returns a promise that fulfills when the piping process completes successfully, or rejects if any errors were encountered.
*
@@ -2103,8 +2107,6 @@ interface StreamPipeOptions {
* The signal option can be set to an AbortSignal to allow aborting an ongoing pipe operation via the corresponding AbortController. In this case, this source readable stream will be canceled, and destination aborted, unless the respective options preventCancel or preventAbort are set.
*/
preventClose?: boolean;
preventAbort?: boolean;
preventCancel?: boolean;
signal?: AbortSignal;
}
type ReadableStreamReadResult<R = any> = {
@@ -2379,13 +2381,13 @@ declare abstract class TransformStreamDefaultController<O = any> {
terminate(): void;
}
interface ReadableWritablePair<R = any, W = any> {
readable: ReadableStream<R>;
/**
* Provides a convenient, chainable way of piping this readable stream through a transform stream (or any other { writable, readable } pair). It simply pipes the stream into the writable side of the supplied pair, and returns the readable side for further use.
*
* Piping a stream will lock it for the duration of the pipe, preventing any other consumer from acquiring a reader.
*/
writable: WritableStream<W>;
readable: ReadableStream<R>;
}
/**
* The **`WritableStream`** interface of the Streams API provides a standard abstraction for writing streaming data to a destination, known as a sink.
@@ -3261,7 +3263,7 @@ interface WorkerStubEntrypointOptions {
props?: any;
}
interface WorkerLoader {
get(name: string, getCode: () => WorkerLoaderWorkerCode | Promise<WorkerLoaderWorkerCode>): WorkerStub;
get(name: string | null, getCode: () => WorkerLoaderWorkerCode | Promise<WorkerLoaderWorkerCode>): WorkerStub;
}
interface WorkerLoaderModule {
js?: string;
@@ -8443,7 +8445,7 @@ type AiOptions = {
* Maximum 5 tags are allowed each request.
* Duplicate tags will removed.
*/
tags: string[];
tags?: string[];
gateway?: GatewayOptions;
returnRawResponse?: boolean;
prefix?: string;