add data table to saladeaula
This commit is contained in:
@@ -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,52 +67,50 @@ export default function Route({ loaderData: { data } }) {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Suspense fallback={<Skeleton />}>
|
||||
<Await resolve={data}>
|
||||
{({ items }) => {
|
||||
return (
|
||||
<div className="grid gap-4 lg:gap-8 md:grid-cols-2 lg:grid-cols-3">
|
||||
{items.map(({ sk, name, email }: Admin) => {
|
||||
const [_, id] = sk.split('#')
|
||||
<Await resolve={data}>
|
||||
{({ items }) => {
|
||||
return (
|
||||
<div className="grid gap-4 lg:gap-8 md:grid-cols-2 lg:grid-cols-3">
|
||||
{items.map(({ sk, name, email }: Admin) => {
|
||||
const [_, id] = sk.split('#')
|
||||
|
||||
return (
|
||||
<section
|
||||
key={id}
|
||||
className="bg-card border-border/50 hover:shadow-muted-foreground/10 hover:border-muted group
|
||||
return (
|
||||
<section
|
||||
key={id}
|
||||
className="bg-card border-border/50 hover:shadow-muted-foreground/10 hover:border-muted group
|
||||
relative overflow-hidden rounded-2xl border p-8 transition-all duration-300 hover:shadow-2xl"
|
||||
>
|
||||
<ActionMenu id={id} />
|
||||
<div
|
||||
className="from-muted-foreground/5 absolute inset-0 bg-gradient-to-br to-transparent
|
||||
>
|
||||
<ActionMenu id={id} />
|
||||
<div
|
||||
className="from-muted-foreground/5 absolute inset-0 bg-gradient-to-br to-transparent
|
||||
opacity-0 transition-opacity duration-300 group-hover:opacity-100"
|
||||
/>
|
||||
<div className="relative flex flex-col items-center text-center">
|
||||
<div className="relative mb-6">
|
||||
<Avatar className="size-24 lg:size-28">
|
||||
<AvatarFallback className="text-2xl">
|
||||
{initials(name)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<h1 className="mb-2 text-xl font-bold">
|
||||
<Abbr>{name}</Abbr>
|
||||
</h1>
|
||||
<p className="text-muted-foreground bg-muted/50 inline-block rounded-full px-4 py-1.5 text-sm font-medium">
|
||||
<Abbr>{email}</Abbr>
|
||||
</p>
|
||||
</div>
|
||||
/>
|
||||
<div className="relative flex flex-col items-center text-center">
|
||||
<div className="relative mb-6">
|
||||
<Avatar className="size-24 lg:size-28">
|
||||
<AvatarFallback className="text-2xl">
|
||||
{initials(name)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</Await>
|
||||
</Suspense>
|
||||
</>
|
||||
|
||||
<div className="mb-6">
|
||||
<h1 className="mb-2 text-xl font-bold">
|
||||
<Abbr>{name}</Abbr>
|
||||
</h1>
|
||||
<p className="text-muted-foreground bg-muted/50 inline-block rounded-full px-4 py-1.5 text-sm font-medium">
|
||||
<Abbr>{email}</Abbr>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</Await>
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,31 +45,29 @@ export default function Route({ loaderData: { data } }) {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Suspense fallback={<Skeleton />}>
|
||||
<Await resolve={data}>
|
||||
{(resolved) => (
|
||||
<>
|
||||
<Empty className="border border-dasheds">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<UploadIcon />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>Nenhum importação ainda</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Importe seus colaboradores para gerenciar, segmentar e
|
||||
facilitar sua gestão.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button>
|
||||
<PlusIcon /> Importar
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
</>
|
||||
)}
|
||||
</Await>
|
||||
</Suspense>
|
||||
</>
|
||||
<Await resolve={data}>
|
||||
{(resolved) => (
|
||||
<>
|
||||
<Empty className="border border-dasheds">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<UploadIcon />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>Nenhum importação ainda</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Importe seus colaboradores para gerenciar, segmentar e
|
||||
facilitar sua gestão.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
<EmptyContent>
|
||||
<Button>
|
||||
<PlusIcon /> Importar
|
||||
</Button>
|
||||
</EmptyContent>
|
||||
</Empty>
|
||||
</>
|
||||
)}
|
||||
</Await>
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
63
apps/saladeaula.digital/app/routes/payments/columns.tsx
Normal file
63
apps/saladeaula.digital/app/routes/payments/columns.tsx
Normal 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} />
|
||||
)
|
||||
}
|
||||
]
|
||||
101
apps/saladeaula.digital/app/routes/payments/route.tsx
Normal file
101
apps/saladeaula.digital/app/routes/payments/route.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user