add certs page

This commit is contained in:
2026-01-27 18:34:13 -03:00
parent 10138112fe
commit 82dc878502
8 changed files with 192 additions and 74 deletions

View File

@@ -54,6 +54,7 @@ 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.certs, prefix='/orgs')
app.include_router(orgs.custom_pricing, prefix='/orgs')
app.include_router(orgs.scheduled, prefix='/orgs')
app.include_router(orgs.submissions, prefix='/orgs')

View File

@@ -10,6 +10,7 @@ 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
from .enrollments.certs import router as certs
from .enrollments.scheduled import router as scheduled
from .enrollments.submissions import router as submissions
from .seats import router as seats
@@ -23,6 +24,7 @@ __all__ = [
'admins',
'billing',
'custom_pricing',
'certs',
'scheduled',
'submissions',
'seats',

View File

@@ -0,0 +1,29 @@
from datetime import date
from typing import Annotated
from aws_lambda_powertools import Logger
from aws_lambda_powertools.event_handler.api_gateway import Router
from aws_lambda_powertools.event_handler.openapi.params import Query
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
from boto3clients import dynamodb_client
from config import ENROLLMENT_TABLE
logger = Logger(__name__)
router = Router()
dyn = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client)
@router.get('/<org_id>/enrollments/certs')
def certs(
org_id: str,
month: Annotated[date, Query()],
):
year_month = month.strftime('%Y-%m')
return dyn.collection.query(
KeyPair(
f'CERT_REPORTING#ORG#{org_id}',
f'MONTH#{year_month}#ENROLLMENT',
),
)

View File

@@ -1,5 +1,5 @@
from http import HTTPStatus
from typing import Annotated, cast
from typing import Annotated
from aws_lambda_powertools import Logger
from aws_lambda_powertools.event_handler.api_gateway import Router
@@ -11,7 +11,6 @@ from pydantic import FutureDatetime
from api_gateway import JSONResponse
from boto3clients import dynamodb_client
from config import ENROLLMENT_TABLE
from routes.orgs import billing
from ...enrollments.enroll import Context, Enrollment, Org, Subscription, enroll_now

View File

@@ -3,6 +3,7 @@
import {
BookCopyIcon,
CalendarClockIcon,
FileBadgeIcon,
// FileBadgeIcon,
GraduationCap,
LayoutDashboardIcon,
@@ -65,11 +66,11 @@ const data = {
url: '/enrollments',
icon: GraduationCap
},
// {
// title: 'Certificações',
// url: '/certs',
// icon: FileBadgeIcon
// },
{
title: 'Certificações',
url: '/certs',
icon: FileBadgeIcon
},
{
title: 'Agendamentos',
url: '/scheduled',

View File

@@ -76,73 +76,71 @@ export default function Route({
const search = searchParams.get('s') as string
return (
<>
<Suspense fallback={<Skeleton />}>
<div className="space-y-0.5 mb-8">
<h1 className="text-2xl font-bold tracking-tight">
Resumo de cobranças
</h1>
<p className="text-muted-foreground">
Acompanhe as cobranças em tempo real e garanta mais eficiência no
controle financeiro.
</p>
</div>
<Suspense fallback={<Skeleton />}>
<div className="space-y-0.5 mb-8">
<h1 className="text-2xl font-bold tracking-tight">
Resumo de cobranças
</h1>
<p className="text-muted-foreground">
Acompanhe as cobranças em tempo real e garanta mais eficiência no
controle financeiro.
</p>
</div>
<Await resolve={billing}>
{({ items = [], ...billing }) => {
const {
icon: Icon,
label: status,
color
} = statuses?.[billing?.status || 'CLOSED']
<Await resolve={billing}>
{({ items = [], ...billing }) => {
const {
icon: Icon,
label: status,
color
} = statuses?.[billing?.status || 'CLOSED']
return (
<Card>
<CardContent className="space-y-4">
<div className="flex max-lg:flex-col gap-2.5">
<div className="w-full xl:w-1/4">
<SearchForm
defaultValue={search || ''}
placeholder={
<>
Digite <Kbd className="border font-mono">/</Kbd>{' '}
para pesquisar
</>
}
onChange={(value) =>
setSearchParams((searchParams) => {
searchParams.set('s', String(value))
return searchParams
})
}
/>
</div>
<RangePeriod
startDate={startDate}
endDate={endDate}
billingDay={billing_day}
return (
<Card>
<CardContent className="space-y-4">
<div className="flex max-lg:flex-col gap-2.5">
<div className="w-full xl:w-1/4">
<SearchForm
defaultValue={search || ''}
placeholder={
<>
Digite <Kbd className="border font-mono">/</Kbd> para
pesquisar
</>
}
onChange={(value) =>
setSearchParams((searchParams) => {
searchParams.set('s', String(value))
return searchParams
})
}
/>
<Button
className={cn('pointer-events-none lg:ml-auto', color)}
variant="outline"
asChild
>
<span>
<Icon className="size-3.5" /> {status}
</span>
</Button>
</div>
<List items={items} search={search} />
</CardContent>
</Card>
)
}}
</Await>
</Suspense>
</>
<RangePeriod
startDate={startDate}
endDate={endDate}
billingDay={billing_day}
/>
<Button
className={cn('pointer-events-none lg:ml-auto', color)}
variant="outline"
asChild
>
<span>
<Icon className="size-3.5" /> {status}
</span>
</Button>
</div>
<List items={items} search={search} />
</CardContent>
</Card>
)
}}
</Await>
</Suspense>
)
}
@@ -217,7 +215,7 @@ function List({ items, search }) {
{charges.length ? (
<>
<TableHeader>
<TableRow className="bg-muted-foreground/10 pointer-events-none">
<TableRow className=" pointer-events-none">
<TableHead>Colaborador</TableHead>
<TableHead>Curso</TableHead>
<TableHead>Matriculado por</TableHead>

View File

@@ -1,12 +1,46 @@
import type { Route } from './+types/route'
import { Suspense } from 'react'
import { Await } from 'react-router'
import { DateTime } from '@repo/ui/components/datetime'
import { Skeleton } from '@repo/ui/components/skeleton'
import { Card, CardContent } from '@repo/ui/components/ui/card'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow
} from '@repo/ui/components/ui/table'
import { request as req } from '@repo/util/request'
export function meta({}) {
return [{ title: 'Certificações' }]
}
export default function Route({}: Route.ComponentProps) {
export async function loader({ context, request, params }: Route.LoaderArgs) {
const { searchParams } = new URL(request.url)
const month =
searchParams.get('month') || new Date().toISOString().slice(0, 10)
const reporting = req({
url: `/orgs/${params.orgid}/enrollments/certs?month=${month}`,
context,
request
}).then((r) => r.json())
return {
reporting
}
}
export default function Route({
loaderData: { reporting }
}: Route.ComponentProps) {
return (
<>
<Suspense fallback={<Skeleton />}>
<div className="space-y-0.5 mb-8">
<h1 className="text-2xl font-bold tracking-tight">
Gerenciar certificações
@@ -16,6 +50,57 @@ export default function Route({}: Route.ComponentProps) {
prazos e renovações com facilidade.
</p>
</div>
</>
<Card>
<CardContent>
<Table>
<TableHeader>
<TableRow className=" pointer-events-none">
<TableHead>Colaborador</TableHead>
<TableHead>Curso</TableHead>
<TableHead>Matriculado em</TableHead>
<TableHead>Concluído em</TableHead>
<TableHead>Cert. válido até</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<Await resolve={reporting}>
{({ items = [] }) => {
return (
<>
{items.map(
({
course,
user,
enrolled_at,
completed_at,
expires_at
}) => {
return (
<TableRow>
<TableCell>{user.name}</TableCell>
<TableCell>{course.name}</TableCell>
<TableCell>
<DateTime>{enrolled_at}</DateTime>
</TableCell>
<TableCell>
<DateTime>{completed_at}</DateTime>
</TableCell>
<TableCell>
<DateTime>{expires_at}</DateTime>
</TableCell>
</TableRow>
)
}
)}
</>
)
}}
</Await>
</TableBody>
</Table>
</CardContent>
</Card>
</Suspense>
)
}

View File

@@ -190,7 +190,10 @@ export default function Route({
<Button size="sm" variant="outline" asChild>
<NavLink to="../enrollments/seats">
{({ isPending }) => (
<>{isPending ? <Spinner /> : <PlusIcon />} Matricular</>
<>
{isPending ? <Spinner /> : <PlusIcon />}
<span className="max-lg:hidden">Matricular</span>
</>
)}
</NavLink>
</Button>