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.address, prefix='/orgs')
app.include_router(orgs.admins, prefix='/orgs') app.include_router(orgs.admins, prefix='/orgs')
app.include_router(orgs.billing, 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.custom_pricing, prefix='/orgs')
app.include_router(orgs.scheduled, prefix='/orgs') app.include_router(orgs.scheduled, prefix='/orgs')
app.include_router(orgs.submissions, 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 .admins import router as admins
from .billing import router as billing from .billing import router as billing
from .custom_pricing import router as custom_pricing from .custom_pricing import router as custom_pricing
from .enrollments.certs import router as certs
from .enrollments.scheduled import router as scheduled from .enrollments.scheduled import router as scheduled
from .enrollments.submissions import router as submissions from .enrollments.submissions import router as submissions
from .seats import router as seats from .seats import router as seats
@@ -23,6 +24,7 @@ __all__ = [
'admins', 'admins',
'billing', 'billing',
'custom_pricing', 'custom_pricing',
'certs',
'scheduled', 'scheduled',
'submissions', 'submissions',
'seats', '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 http import HTTPStatus
from typing import Annotated, cast from typing import Annotated
from aws_lambda_powertools import Logger from aws_lambda_powertools import Logger
from aws_lambda_powertools.event_handler.api_gateway import Router from aws_lambda_powertools.event_handler.api_gateway import Router
@@ -11,7 +11,6 @@ from pydantic import FutureDatetime
from api_gateway import JSONResponse from api_gateway import JSONResponse
from boto3clients import dynamodb_client from boto3clients import dynamodb_client
from config import ENROLLMENT_TABLE from config import ENROLLMENT_TABLE
from routes.orgs import billing
from ...enrollments.enroll import Context, Enrollment, Org, Subscription, enroll_now from ...enrollments.enroll import Context, Enrollment, Org, Subscription, enroll_now

View File

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

View File

@@ -76,7 +76,6 @@ export default function Route({
const search = searchParams.get('s') as string const search = searchParams.get('s') as string
return ( return (
<>
<Suspense fallback={<Skeleton />}> <Suspense fallback={<Skeleton />}>
<div className="space-y-0.5 mb-8"> <div className="space-y-0.5 mb-8">
<h1 className="text-2xl font-bold tracking-tight"> <h1 className="text-2xl font-bold tracking-tight">
@@ -105,8 +104,8 @@ export default function Route({
defaultValue={search || ''} defaultValue={search || ''}
placeholder={ placeholder={
<> <>
Digite <Kbd className="border font-mono">/</Kbd>{' '} Digite <Kbd className="border font-mono">/</Kbd> para
para pesquisar pesquisar
</> </>
} }
onChange={(value) => onChange={(value) =>
@@ -142,7 +141,6 @@ export default function Route({
}} }}
</Await> </Await>
</Suspense> </Suspense>
</>
) )
} }
@@ -217,7 +215,7 @@ function List({ items, search }) {
{charges.length ? ( {charges.length ? (
<> <>
<TableHeader> <TableHeader>
<TableRow className="bg-muted-foreground/10 pointer-events-none"> <TableRow className=" pointer-events-none">
<TableHead>Colaborador</TableHead> <TableHead>Colaborador</TableHead>
<TableHead>Curso</TableHead> <TableHead>Curso</TableHead>
<TableHead>Matriculado por</TableHead> <TableHead>Matriculado por</TableHead>

View File

@@ -1,12 +1,46 @@
import type { Route } from './+types/route' 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({}) { export function meta({}) {
return [{ title: 'Certificações' }] 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 ( return (
<> <Suspense fallback={<Skeleton />}>
<div className="space-y-0.5 mb-8"> <div className="space-y-0.5 mb-8">
<h1 className="text-2xl font-bold tracking-tight"> <h1 className="text-2xl font-bold tracking-tight">
Gerenciar certificações Gerenciar certificações
@@ -16,6 +50,57 @@ export default function Route({}: Route.ComponentProps) {
prazos e renovações com facilidade. prazos e renovações com facilidade.
</p> </p>
</div> </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> <Button size="sm" variant="outline" asChild>
<NavLink to="../enrollments/seats"> <NavLink to="../enrollments/seats">
{({ isPending }) => ( {({ isPending }) => (
<>{isPending ? <Spinner /> : <PlusIcon />} Matricular</> <>
{isPending ? <Spinner /> : <PlusIcon />}
<span className="max-lg:hidden">Matricular</span>
</>
)} )}
</NavLink> </NavLink>
</Button> </Button>