diff --git a/api.saladeaula.digital/app/keep_warm.py b/api.saladeaula.digital/app/keep_warm.py index ea9bd38..40dd622 100644 --- a/api.saladeaula.digital/app/keep_warm.py +++ b/api.saladeaula.digital/app/keep_warm.py @@ -12,6 +12,7 @@ logger = Logger(__name__) tracer = Tracer() urls = [ 'https://bcs7fgb9og.execute-api.sa-east-1.amazonaws.com/health', + # 'https://api.saladeaula.digital/health', 'https://id.saladeaula.digital/health', ] diff --git a/api.saladeaula.digital/app/routes/courses/__init__.py b/api.saladeaula.digital/app/routes/courses/__init__.py index bb8d29c..a626c0c 100644 --- a/api.saladeaula.digital/app/routes/courses/__init__.py +++ b/api.saladeaula.digital/app/routes/courses/__init__.py @@ -64,9 +64,7 @@ def put_course(course_id: str): event = router.current_event if not event.decoded_body: - raise BadRequestError('Invalid request body') - - now_ = now() + raise BadRequestError('Invalid request body') now_ = now() body = parse( event.headers, BytesIO(event.decoded_body.encode()), diff --git a/api.saladeaula.digital/app/routes/enrollments/cancel.py b/api.saladeaula.digital/app/routes/enrollments/cancel.py index f1d7ee5..ea2b395 100644 --- a/api.saladeaula.digital/app/routes/enrollments/cancel.py +++ b/api.saladeaula.digital/app/routes/enrollments/cancel.py @@ -1,6 +1,13 @@ +from typing import Annotated + from aws_lambda_powertools import Logger from aws_lambda_powertools.event_handler.api_gateway import Router -from layercake.dynamodb import DynamoDBPersistenceLayer +from aws_lambda_powertools.event_handler.exceptions import ( + BadRequestError, +) +from aws_lambda_powertools.event_handler.openapi.params import Body +from layercake.dateutils import now +from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair from boto3clients import dynamodb_client from config import ENROLLMENT_TABLE @@ -10,5 +17,78 @@ router = Router() dyn = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client) -@router.patch('/') -def cancel(): ... +class CancelPolicyConflictError(BadRequestError): + def __init__(self, *_): + super().__init__('Cancellation policy not found') + + +class SlotConflictError(BadRequestError): + def __init__(self, *_): + super().__init__('Slot not found') + + +@router.post('//cancel') +def cancel( + enrollment_id: str, + lock_hash: Annotated[str | None, Body(embed=True)] = None, +): + now_ = now() + + with dyn.transact_writer() as transact: + transact.update( + key=KeyPair(enrollment_id, '0'), + cond_expr='#status = pending', + update_expr='SET #status = :canceled, \ + canceled_at = :now, \ + updated_at = :now', + expr_attr_names={ + ':status': 'status', + }, + expr_attr_values={ + ':pending': 'PENDING', + ':canceled': 'CANCELED', + ':true': True, + ':now': now_, + }, + ) + transact.put( + item={ + 'id': enrollment_id, + 'sk': 'CANCELED_BY', + 'canceled_by': {}, + 'created_at': now_, + } + ) + transact.delete( + key=KeyPair( + pk=enrollment_id, + sk='CANCEL_POLICY', + ), + cond_expr='attribute_exists(sk)', + exc_cls=CancelPolicyConflictError, + ) + # Remove reminders and policies that no longer apply + transact.delete( + key=KeyPair( + pk=enrollment_id, + sk='SCHEDULE#REMINDER_NO_ACCESS_AFTER_3_DAYS', + ) + ) + transact.delete( + key=KeyPair( + pk=enrollment_id, + sk='SCHEDULE#REMINDER_ACCESS_PERIOD_BEFORE_30_DAYS', + ) + ) + transact.delete( + key=KeyPair( + pk=enrollment_id, + sk='METADATA#PARENT_SLOT', + ), + cond_expr='attribute_exists(sk)', + exc_cls=SlotConflictError, + ) + + if lock_hash: + transact.delete(key=KeyPair(enrollment_id, 'LOCK')) + transact.delete(key=KeyPair('LOCK', lock_hash)) diff --git a/api.saladeaula.digital/app/routes/enrollments/dedup_window.py b/api.saladeaula.digital/app/routes/enrollments/dedup_window.py index b723146..87aeeb3 100644 --- a/api.saladeaula.digital/app/routes/enrollments/dedup_window.py +++ b/api.saladeaula.digital/app/routes/enrollments/dedup_window.py @@ -16,7 +16,7 @@ router = Router() dyn = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client) -@router.patch('//dedupwindow', compress=True) +@router.delete('//dedupwindow', compress=True) def dedup_window( enrollment_id: str, lock_hash: Annotated[str, Body(embed=True)], diff --git a/apps/admin.saladeaula.digital/app/components/app-sidebar.tsx b/apps/admin.saladeaula.digital/app/components/app-sidebar.tsx index 16b9c7a..2dc92fd 100644 --- a/apps/admin.saladeaula.digital/app/components/app-sidebar.tsx +++ b/apps/admin.saladeaula.digital/app/components/app-sidebar.tsx @@ -8,6 +8,7 @@ import { GraduationCap, LayoutDashboard, ShieldUserIcon, + UploadIcon, UsersIcon } from 'lucide-react' @@ -31,7 +32,9 @@ const data = { title: 'Histórico de compras', url: '/orders', icon: DollarSign - }, + } + ], + navUser: [ { title: 'Colaboradores', url: '/users', @@ -41,9 +44,14 @@ const data = { title: 'Gestores', url: '/admins', icon: ShieldUserIcon + }, + { + title: 'Importações', + url: '/batch', + icon: UploadIcon } ], - navContent: [ + navEnrollment: [ { title: 'Matrículas', url: '/enrollments', diff --git a/apps/admin.saladeaula.digital/app/components/data-table/column-datetime.tsx b/apps/admin.saladeaula.digital/app/components/data-table/column-datetime.tsx new file mode 100644 index 0000000..e69de29 diff --git a/apps/admin.saladeaula.digital/app/components/nav-main.tsx b/apps/admin.saladeaula.digital/app/components/nav-main.tsx index 3c72278..60a23b1 100644 --- a/apps/admin.saladeaula.digital/app/components/nav-main.tsx +++ b/apps/admin.saladeaula.digital/app/components/nav-main.tsx @@ -25,7 +25,8 @@ export function NavMain({ }: { data: { navMain: NavItem[] - navContent: NavItem[] + navUser: NavItem[] + navEnrollment: NavItem[] } }) { return ( @@ -43,8 +44,23 @@ export function NavMain({ - Gestão de matrículas - {data.navContent.map((props, idx) => ( + + Colaboradores + + {data.navUser.map((props, idx) => ( + + ))} + + + + + + + + + Gestão de matrículas + + {data.navEnrollment.map((props, idx) => ( ))} diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.batch._index/route.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.batch._index/route.tsx new file mode 100644 index 0000000..4e2e2a2 --- /dev/null +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.batch._index/route.tsx @@ -0,0 +1,45 @@ +import type { Route } from './+types' + +import { Suspense } from 'react' +import { Await } from 'react-router' + +import { Skeleton } from '@repo/ui/components/skeleton' +import { request as req } from '@repo/util/request' + +export function meta({}: Route.MetaArgs) { + return [{ title: ' Importações de colaboradores' }] +} + +export async function loader({ context, request, params }: Route.LoaderArgs) { + const data = req({ + url: `/orgs/${params.orgid}/enrollments/scheduled`, + context, + request + }).then((r) => r.json()) + + return { + data + } +} + +export default function Route({ loaderData: { data } }) { + return ( + <> +
+

+ Importações de colaboradores +

+

+ Acompanhe suas importações de colaboradores, faça novas importações + sempre que precisar e finalize o processo com rapidez. +

+
+ + }> + + {(resolved) => <>...{console.log(resolved)}} + + + + ) +} diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.$id._index/route.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.$id._index/route.tsx new file mode 100644 index 0000000..083df46 --- /dev/null +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments.$id._index/route.tsx @@ -0,0 +1,54 @@ +import type { Route } from './+types' + +import { useNavigate } from 'react-router' + +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle +} from '@repo/ui/components/ui/dialog' +import { request as req } from '@repo/util/request' + +export async function loader({ params, request, context }: Route.LoaderArgs) { + const { id } = params + const r = await req({ + url: `/enrollments/${id}`, + request, + context + }) + + if (!r.ok) { + throw new Response(null, { status: r.status }) + } + + const enrollment = await r.json() + return { data: enrollment } +} + +export default function UserModal({ loaderData }: Route.ComponentProps) { + const navigate = useNavigate() + const { enrollment } = loaderData + + return ( + { + if (!open) navigate('/enrollments') // Volta pra listagem ao fechar + }} + > + + + ... + Detalhes do usuário + + +
+ ... + {/* Mais informações... */} +
+
+
+ ) +} diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments._index/columns.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments._index/columns.tsx index d8de1ad..0770ad9 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments._index/columns.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments._index/columns.tsx @@ -1,16 +1,46 @@ 'use client' import type { CellContext, ColumnDef } from '@tanstack/react-table' -import { HelpCircleIcon } from 'lucide-react' +import { useToggle } from 'ahooks' +import { + CircleXIcon, + EllipsisVerticalIcon, + FileBadgeIcon, + HelpCircleIcon, + LockOpenIcon +} from 'lucide-react' +import { NavLink, useParams } from 'react-router' +import { toast } from 'sonner' +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger +} from '@repo/ui/components/ui/alert-dialog' import { Avatar, AvatarFallback } from '@repo/ui/components/ui/avatar' import { Badge } from '@repo/ui/components/ui/badge' +import { Button } from '@repo/ui/components/ui/button' import { Checkbox } from '@repo/ui/components/ui/checkbox' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger +} from '@repo/ui/components/ui/dropdown-menu' import { Progress } from '@repo/ui/components/ui/progress' +import { Spinner } from '@repo/ui/components/ui/spinner' import { cn, initials } from '@repo/ui/lib/utils' import { Abbr } from '@/components/abbr' import { DataTableColumnHeader } from '@/components/data-table/column-header' +import { useDataTable } from '@/components/data-table/data-table' import { labels, statuses } from './data' // This type is used to define the shape of our data. @@ -167,6 +197,10 @@ export const columns: ColumnDef[] = [ enableSorting: true, enableHiding: true, cell: cellDate + }, + { + id: 'actions', + cell: ({ row }) => } ] @@ -183,3 +217,159 @@ function cellDate({ return <> } + +function ActionMenu({ row }: { row: any }) { + const cert = row.original?.cert + const progress = row.getValue('progress') + + return ( +
+ + + + + + + + 0} /> + 0} /> + + +
+ ) +} + +function DownloadItem({ id, ...props }: { id: string }) { + const [loading, { set }] = useToggle(false) + + const download = async (e) => { + e.preventDefault() + set(true) + const r = await fetch(`/~/api/enrollments/${id}/download`, { + method: 'GET' + }) + + if (r.ok) { + const { presigned_url } = (await r.json()) as { presigned_url: string } + window.open(presigned_url, '_blank') + } + set(false) + } + + return ( + + {loading ? : } Baixar certificado + + ) +} + +function RemoveDedupItem({ id, ...props }: { id: string }) { + const [loading, { set }] = useToggle(false) + const { orgid } = useParams() + const { table } = useDataTable() + + const cancel = async (e) => { + e.preventDefault() + set(true) + + const r = await fetch(`/~/api/enrollments/${orgid}/dedupwindow`, { + method: 'DELETE' + }) + + if (r.ok) { + toast.info('A proteção contra duplicação foi removida') + // @ts-ignore + table.options.meta?.removeRow?.(id) + } + } + + return ( + + + e.preventDefault()} + {...props} + > + Remover proteção + + + + + Tem certeza absoluta? + + Esta ação não pode ser desfeita. Isso remove a proteção contra + duplicação permanentemente desta matrícula. + + + + Cancelar + + + + + + + ) +} + +function CancelItem({ id, ...props }: { id: string }) { + const [loading, { set }] = useToggle(false) + const { orgid } = useParams() + const { table } = useDataTable() + + const cancel = async (e) => { + e.preventDefault() + set(true) + + const r = await fetch(`/~/api/enrollments/${orgid}/cancel`, { + method: 'PATCH' + }) + + if (r.ok) { + toast.info('A matrícula foi cancelada') + // @ts-ignore + table.options.meta?.removeRow?.(id) + } + } + + return ( + + + e.preventDefault()} + {...props} + > + Cancelar + + + + + Tem certeza absoluta? + + Esta ação não pode ser desfeita. Isso cancelar permanentemente a + matrícula deste colaborador. + + + + Cancelar + + + + + + + ) +} diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments._index/route.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments._index/route.tsx index 74020d9..c51e42a 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments._index/route.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.enrollments._index/route.tsx @@ -12,7 +12,7 @@ import { } from 'lucide-react' import { MeiliSearchFilterBuilder } from 'meilisearch-helper' import { Suspense, useState } from 'react' -import { Await, Link, useParams, useSearchParams } from 'react-router' +import { Await, Link, Outlet, useParams, useSearchParams } from 'react-router' import type { BookType } from 'xlsx' import * as XLSX from 'xlsx' @@ -221,6 +221,8 @@ export default function Route({ loaderData: { data } }) { )} + + ) } diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.users._index/columns.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.users._index/columns.tsx index 585d119..b1e60ab 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.users._index/columns.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.users._index/columns.tsx @@ -1,7 +1,7 @@ 'use client' import { formatCPF } from '@brazilian-utils/brazilian-utils' -import { type ColumnDef, type RowData } from '@tanstack/react-table' +import { type ColumnDef } from '@tanstack/react-table' import { useToggle } from 'ahooks' import { EllipsisVerticalIcon, diff --git a/enrollments-events/app/events/schedule_reminders.py b/enrollments-events/app/events/schedule_reminders.py index cd6d619..ad49596 100644 --- a/enrollments-events/app/events/schedule_reminders.py +++ b/enrollments-events/app/events/schedule_reminders.py @@ -52,14 +52,14 @@ def lambda_handler(event: EventBridgeEvent, context: LambdaContext) -> bool: ) # Schedule the event to add `access_expired` after the access period ends - transact.put( - item={ - 'id': enrollment_id, - 'sk': 'SCHEDULE#SET_ACCESS_EXPIRED', - 'created_at': now_, - 'ttl': ttl(start_dt=now_, days=access_period), - }, - ) + # transact.put( + # item={ + # 'id': enrollment_id, + # 'sk': 'SCHEDULE#SET_ACCESS_EXPIRED', + # 'created_at': now_, + # 'ttl': ttl(start_dt=now_, days=access_period), + # }, + # ) transact.put( item={