add data table to saladeaula

This commit is contained in:
2025-11-24 15:31:36 -03:00
parent 97573868b7
commit 1b5d331c36
9 changed files with 268 additions and 186 deletions

View File

@@ -59,7 +59,7 @@ export async function loader({ context, request, params }: Route.LoaderArgs) {
export default function Route({ loaderData: { data } }) {
return (
<>
<Suspense fallback={<Skeleton />}>
<div className="space-y-0.5 mb-8">
<h1 className="text-2xl font-bold tracking-tight">Gestores</h1>
<p className="text-muted-foreground">
@@ -67,7 +67,6 @@ export default function Route({ loaderData: { data } }) {
</p>
</div>
<Suspense fallback={<Skeleton />}>
<Await resolve={data}>
{({ items }) => {
return (
@@ -112,7 +111,6 @@ export default function Route({ loaderData: { data } }) {
}}
</Await>
</Suspense>
</>
)
}

View File

@@ -34,7 +34,7 @@ export async function loader({ context, request, params }: Route.LoaderArgs) {
export default function Route({ loaderData: { data } }) {
return (
<>
<Suspense fallback={<Skeleton />}>
<div className="space-y-0.5 mb-8">
<h1 className="text-2xl font-bold tracking-tight">
Importações de colaboradores
@@ -45,7 +45,6 @@ export default function Route({ loaderData: { data } }) {
</p>
</div>
<Suspense fallback={<Skeleton />}>
<Await resolve={data}>
{(resolved) => (
<>
@@ -70,6 +69,5 @@ export default function Route({ loaderData: { data } }) {
)}
</Await>
</Suspense>
</>
)
}

View File

@@ -27,9 +27,8 @@ export async function loader({ params, request, context }: Route.LoaderArgs) {
return { data: enrollment }
}
export default function UserModal({ loaderData }: Route.ComponentProps) {
export default function UserModal({}: Route.ComponentProps) {
const navigate = useNavigate()
const { enrollment } = loaderData
return (
<Dialog

View File

@@ -9,7 +9,7 @@ export default [
layout('routes/layout.tsx', [
index('routes/index.tsx'),
route('certs', 'routes/certs.tsx'),
route('payments', 'routes/payments.tsx'),
route('payments', 'routes/payments/route.tsx'),
route('settings', 'routes/settings.tsx'),
route('konviva', 'routes/konviva.ts'),
route('player/:id', 'routes/player.tsx'),

View File

@@ -1,6 +1,37 @@
import { redirect } from 'react-router'
import type { Route } from './+types'
import { userContext } from '@repo/auth/context'
import type { User } from '@repo/auth/auth'
export async function loader({ params, context }: Route.LoaderArgs) {
return redirect('https://lms.saladeaula.digital')
const konvivaApi = 'https://lms.saladeaula.digital'
export async function loader({ context }: Route.LoaderArgs) {
const user = context.get(userContext) as User
const secretKey = context.cloudflare.env.KONVIVA_SECRET_KEY
const token = await getToken(user.email, secretKey)
const url = new URL('/action/acessoExterno', konvivaApi)
url.search = new URLSearchParams(token).toString()
return redirect(url.toString())
}
async function getToken(
email: string,
secretKey: string
): Promise<Record<string, string>> {
const headers = new Headers({
Authorization: `KONVIVA ${secretKey}`,
'Content-Type': 'application/json'
})
const url = new URL(`/action/api/usuarios/token`, konvivaApi)
url.search = new URLSearchParams({ login: email }).toString()
const r = await fetch(url.toString(), { method: 'GET', headers })
if (!r.ok) {
throw new Error(await r.text())
}
return await r.json()
}

View File

@@ -1,109 +0,0 @@
import type { Route } from './+types'
import { MeiliSearchFilterBuilder } from 'meilisearch-helper'
import { Await, Link } from 'react-router'
import { type User } from '@repo/auth/auth'
import { userContext } from '@repo/auth/context'
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator
} from '@repo/ui/components/ui/breadcrumb'
import { createSearch } from '@repo/util/meili'
import { Container } from '@/components/container'
import { Skeleton } from '@repo/ui/components/skeleton'
import { Card, CardContent } from '@repo/ui/components/ui/card'
import { Suspense } from 'react'
export function meta({}: Route.MetaArgs) {
return [{ title: 'Histórico de compras' }]
}
export const loader = async ({ request, context }: Route.ActionArgs) => {
const user = context.get(userContext) as User
const { searchParams } = new URL(request.url)
const status = searchParams.getAll('status') || []
let builder = new MeiliSearchFilterBuilder().where('user_id', '=', user.sub)
if (status.length) {
builder = builder.where('status', 'in', status)
}
const payments = createSearch({
index: 'betaeducacao-prod-orders',
filter: builder.build(),
sort: ['create_date:desc'],
env: context.cloudflare.env
})
return {
data: payments
}
}
export default function Component({ loaderData: { data } }) {
return (
<Container className="space-y-2.5">
<Suspense fallback={<Skeleton />}>
<Await resolve={data}>
{({ hits }) => (
<>
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link to="..">Meus cursos</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Histórico de compras</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<div className="space-y-0.5 mb-8">
<h1 className="text-2xl font-bold tracking-tight">
Histórico de compras
</h1>
<p className="text-muted-foreground">
Acompanhe todos as compras realizadas, visualize pagamentos e
mantenha o controle financeiro.
</p>
</div>
<Card>
<CardContent>
<table className="table-fixed">
<thead>
<tr>
<th>Forma de pag.</th>
<th>Status</th>
<th>Total</th>
<th>Comprador em</th>
<th>Vencimento em</th>
</tr>
{hits.map(({ payment_method, status }) => {
return (
<tr>
<td>{payment_method}</td>
<td>{status}</td>
</tr>
)
})}
</thead>
</table>
</CardContent>
</Card>
</>
)}
</Await>
</Suspense>
</Container>
)
}

View File

@@ -0,0 +1,63 @@
'use client'
import {
DataTableColumnDatetime,
DataTableColumnCurrency,
DataTableColumnHeader
} from '@repo/ui/components/data-table'
import { type ColumnDef } from '@tanstack/react-table'
// This type is used to define the shape of our data.
// You can use a Zod schema here if you want.
export type Order = {
id: string
total: number
status: 'pending' | 'processing' | 'success' | 'failed'
payment_method: 'PIX' | 'CREDIT_CARD' | 'MANUAL' | 'failed'
name: string
}
export const columns: ColumnDef<Order>[] = [
{
accessorKey: 'payment_method',
header: 'Forma de pag.'
},
{
accessorKey: 'status',
header: 'Status'
},
{
accessorKey: 'total',
header: 'Valor total',
cell: ({ row, column }) => (
<DataTableColumnCurrency row={row} column={column} />
)
},
{
accessorKey: 'create_date',
enableSorting: true,
meta: { title: 'Comprado em' },
header: ({ column }) => <DataTableColumnHeader column={column} />,
cell: ({ row, column }) => (
<DataTableColumnDatetime row={row} column={column} />
)
},
{
accessorKey: 'due_date',
enableSorting: true,
meta: { title: 'Vencimento em' },
header: ({ column }) => <DataTableColumnHeader column={column} />,
cell: ({ row, column }) => (
<DataTableColumnDatetime row={row} column={column} />
)
},
{
accessorKey: 'payment_date',
enableSorting: true,
meta: { title: 'Pago em' },
header: ({ column }) => <DataTableColumnHeader column={column} />,
cell: ({ row, column }) => (
<DataTableColumnDatetime row={row} column={column} />
)
}
]

View File

@@ -0,0 +1,101 @@
import type { Route } from '../+types'
import { MeiliSearchFilterBuilder } from 'meilisearch-helper'
import { Await, Link } from 'react-router'
import { Suspense } from 'react'
import { type User } from '@repo/auth/auth'
import { userContext } from '@repo/auth/context'
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator
} from '@repo/ui/components/ui/breadcrumb'
import { createSearch } from '@repo/util/meili'
import { Skeleton } from '@repo/ui/components/skeleton'
import { DataTable } from '@repo/ui/components/data-table'
import { Container } from '@/components/container'
import { columns, type Order } from './columns'
export function meta({}: Route.MetaArgs) {
return [{ title: 'Histórico de compras' }]
}
export const loader = async ({ request, context }: Route.ActionArgs) => {
const user = context.get(userContext) as User
const { searchParams } = new URL(request.url)
const status = searchParams.getAll('status') || []
const sort = searchParams.get('sort') || 'create_date:desc'
const page = Number(searchParams.get('p')) + 1
const hitsPerPage = Number(searchParams.get('perPage')) || 25
let builder = new MeiliSearchFilterBuilder().where('user_id', '=', user.sub)
if (status.length) {
builder = builder.where('status', 'in', status)
}
const payments = createSearch({
index: 'betaeducacao-prod-orders',
filter: builder.build(),
sort: [sort],
page,
hitsPerPage,
env: context.cloudflare.env
})
return {
data: payments
}
}
export default function Component({ loaderData: { data } }) {
return (
<Suspense fallback={<Skeleton />}>
<Container className="space-y-2.5">
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link to="..">Meus cursos</Link>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Histórico de compras</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<div className="space-y-0.5 mb-8">
<h1 className="text-2xl font-bold tracking-tight">
Histórico de compras
</h1>
<p className="text-muted-foreground">
Acompanhe todos as compras realizadas, visualize pagamentos e
mantenha o controle financeiro.
</p>
</div>
<Await resolve={data}>
{({ hits, page, hitsPerPage, totalHits }) => {
return (
<DataTable
sort={[{ id: 'create_date', desc: true }]}
columns={columns}
data={hits as Order[]}
pageIndex={page - 1}
pageSize={hitsPerPage}
rowCount={totalHits}
/>
)
}}
</Await>
</Container>
</Suspense>
)
}

View File

@@ -1,5 +1,5 @@
/* eslint-disable */
// Generated by Wrangler by running `wrangler types` (hash: a901705264ceb3e725e174d55b20e764)
// Generated by Wrangler by running `wrangler types` (hash: 46c0a3878ceeb71c97bee6d456de6815)
// Runtime types generated with workerd@1.20251113.0 2025-04-04 nodejs_compat
declare namespace Cloudflare {
interface GlobalProps {
@@ -17,6 +17,7 @@ declare namespace Cloudflare {
REDIRECT_URI: string;
SESSION_SECRET: string;
MEILI_API_KEY: string;
KONVIVA_SECRET_KEY: string;
}
}
interface Env extends Cloudflare.Env {}
@@ -24,7 +25,7 @@ type StringifyValues<EnvType extends Record<string, unknown>> = {
[Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string;
};
declare namespace NodeJS {
interface ProcessEnv extends StringifyValues<Pick<Cloudflare.Env, "CLIENT_ID" | "SCOPE" | "API_URL" | "ISSUER_URL" | "BUCKET_NAME" | "BUCKET_ENDPOINT" | "MEILI_HOST" | "CLIENT_SECRET" | "REDIRECT_URI" | "SESSION_SECRET" | "MEILI_API_KEY">> {}
interface ProcessEnv extends StringifyValues<Pick<Cloudflare.Env, "CLIENT_ID" | "SCOPE" | "API_URL" | "ISSUER_URL" | "BUCKET_NAME" | "BUCKET_ENDPOINT" | "MEILI_HOST" | "CLIENT_SECRET" | "REDIRECT_URI" | "SESSION_SECRET" | "MEILI_API_KEY" | "KONVIVA_SECRET_KEY">> {}
}
// Begin runtime types