add rybbit

This commit is contained in:
2025-12-20 22:23:35 -03:00
parent f0307d4603
commit 43e6973f88
17 changed files with 269 additions and 115 deletions

View File

@@ -1,7 +1,7 @@
from aws_lambda_powertools import Logger
from aws_lambda_powertools.event_handler.api_gateway import Router
from aws_lambda_powertools.event_handler.exceptions import NotFoundError
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair
from layercake.dynamodb import DynamoDBPersistenceLayer, KeyPair, PartitionKey
from boto3clients import dynamodb_client
from config import ENROLLMENT_TABLE
@@ -11,6 +11,14 @@ router = Router()
dyn = DynamoDBPersistenceLayer(ENROLLMENT_TABLE, dynamodb_client)
@router.get('/<org_id>/enrollments/submissions')
def submissions(org_id: str):
return dyn.collection.query(
PartitionKey(f'SUBMISSION#ORG#{org_id}'),
projection_expr='id, sk, created_by',
)
@router.get('/<org_id>/enrollments/<submission_id>/submitted')
def submitted(org_id: str, submission_id: str):
return dyn.collection.get_item(

View File

@@ -26,7 +26,7 @@ Globals:
Architectures:
- x86_64
Layers:
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:100
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:103
Environment:
Variables:
TZ: America/Sao_Paulo

View File

@@ -667,7 +667,7 @@ wheels = [
[[package]]
name = "layercake"
version = "0.11.3"
version = "0.11.4"
source = { directory = "../layercake" }
dependencies = [
{ name = "arnparse" },

View File

@@ -0,0 +1,53 @@
interface Rybbit {
/**
* Tracks a page view
*/
pageview: () => void
/**
* Tracks a custom event
* @param name Name of the event
* @param properties Optional properties for the event
*/
event: (name: string, properties?: Record<string, any>) => void
/**
* Sets a custom user ID for tracking logged-in users
* @param userId The user ID to set (will be stored in localStorage)
* @param traits Optional user metadata (email, name, custom fields)
*/
identify: (userId: string, traits?: Record<string, unknown>) => void
/**
* Updates traits for the currently identified user
* @param traits User metadata to merge with existing traits
*/
setTraits: (traits: Record<string, unknown>) => void
/**
* Clears the stored user ID
*/
clearUserId: () => void
/**
* Gets the currently set user ID
* @returns The current user ID or null if not set
*/
getUserId: () => string | null
/**
* Manually tracks outbound link clicks
* @param url The URL of the outbound link
* @param text Optional text content of the link
* @param target Optional target attribute of the link
*/
trackOutbound: (url: string, text?: string, target?: string) => void
}
declare global {
interface Window {
rybbit: Rybbit
}
}
export {}

View File

@@ -208,102 +208,100 @@ function List({ items, search }) {
}
return (
<div className="border rounded-lg overflow-hidden">
<Table className="table-auto w-full">
{charges.length ? (
<>
<TableHeader>
<TableRow className="bg-muted-foreground/10 pointer-events-none">
<TableHead>Colaborador</TableHead>
<TableHead>Curso</TableHead>
<TableHead>Matriculado por</TableHead>
<TableHead>Matriculado em</TableHead>
<TableHead>Valor unit.</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{charges?.map(
(
{ user, course, author: created_by, unit_price, enrolled_at },
index
) => (
<TableRow key={index}>
<TableCell>
<Abbr>{user.name}</Abbr>
</TableCell>
<TableCell>
<Abbr>{course.name}</Abbr>
</TableCell>
<TableCell>
<Abbr>{created_by ? created_by.name : 'N/A'}</Abbr>
</TableCell>
<TableCell>
<DateTime>{enrolled_at}</DateTime>
</TableCell>
<TableCell>
<Currency>{unit_price}</Currency>
</TableCell>
</TableRow>
)
)}
</TableBody>
</>
) : null}
<Table className="table-auto w-full">
{charges.length ? (
<>
<TableHeader>
<TableRow className="bg-muted-foreground/10 pointer-events-none">
<TableHead>Colaborador</TableHead>
<TableHead>Curso</TableHead>
<TableHead>Matriculado por</TableHead>
<TableHead>Matriculado em</TableHead>
<TableHead>Valor unit.</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{charges?.map(
(
{ user, course, author: created_by, unit_price, enrolled_at },
index
) => (
<TableRow key={index}>
<TableCell>
<Abbr>{user.name}</Abbr>
</TableCell>
<TableCell>
<Abbr>{course.name}</Abbr>
</TableCell>
<TableCell>
<Abbr>{created_by ? created_by.name : 'N/A'}</Abbr>
</TableCell>
<TableCell>
<DateTime>{enrolled_at}</DateTime>
</TableCell>
<TableCell>
<Currency>{unit_price}</Currency>
</TableCell>
</TableRow>
)
)}
</TableBody>
</>
) : null}
{credits.length ? (
<>
<TableHeader>
<TableRow className="bg-muted-foreground/10 pointer-events-none border-t">
<TableHead colSpan={2}></TableHead>
<TableHead>Cancelado por</TableHead>
<TableHead colSpan={2}>Cancelado em</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{credits?.map(
(
{ user, course, author: canceled_by, unit_price, created_at },
index
) => (
<TableRow key={index}>
<TableCell>
<Abbr>{user.name}</Abbr>
</TableCell>
<TableCell>
<Abbr>{course.name}</Abbr>
</TableCell>
<TableCell>
<Abbr>{canceled_by ? canceled_by.name : 'N/A'}</Abbr>
</TableCell>
<TableCell>
<DateTime>{created_at}</DateTime>
</TableCell>
<TableCell>
<Currency>{unit_price}</Currency>
</TableCell>
</TableRow>
)
)}
</TableBody>
</>
) : null}
{credits.length ? (
<>
<TableHeader>
<TableRow className="bg-muted-foreground/10 pointer-events-none border-t">
<TableHead colSpan={2}></TableHead>
<TableHead>Cancelado por</TableHead>
<TableHead colSpan={2}>Cancelado em</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{credits?.map(
(
{ user, course, author: canceled_by, unit_price, created_at },
index
) => (
<TableRow key={index}>
<TableCell>
<Abbr>{user.name}</Abbr>
</TableCell>
<TableCell>
<Abbr>{course.name}</Abbr>
</TableCell>
<TableCell>
<Abbr>{canceled_by ? canceled_by.name : 'N/A'}</Abbr>
</TableCell>
<TableCell>
<DateTime>{created_at}</DateTime>
</TableCell>
<TableCell>
<Currency>{unit_price}</Currency>
</TableCell>
</TableRow>
)
)}
</TableBody>
</>
) : null}
<TableFooter>
<TableRow>
<TableCell colSpan={4} className="text-right pointer-events-none">
Total
</TableCell>
<TableCell>
<Currency>
{filtered
?.filter((x) => 'course' in x)
?.reduce((acc, { unit_price }) => acc + unit_price, 0)}
</Currency>
</TableCell>
</TableRow>
</TableFooter>
</Table>
</div>
<TableFooter>
<TableRow>
<TableCell colSpan={4} className="text-right pointer-events-none">
Total
</TableCell>
<TableCell>
<Currency>
{filtered
?.filter((x) => 'course' in x)
?.reduce((acc, { unit_price }) => acc + unit_price, 0)}
</Currency>
</TableCell>
</TableRow>
</TableFooter>
</Table>
)
}

View File

@@ -1,6 +1,7 @@
import type { Route } from './+types/route'
import { useToggle } from 'ahooks'
import Fuse from 'fuse.js'
import { useRequest, useToggle } from 'ahooks'
import { ErrorMessage } from '@hookform/error-message'
import {
CalendarIcon,
@@ -15,7 +16,8 @@ import {
ArrowDownAZIcon,
ArrowUpAZIcon,
AlertTriangleIcon,
UserIcon
UserIcon,
EllipsisIcon
} from 'lucide-react'
import { redirect, Link, useParams, useFetcher } from 'react-router'
import { Controller, useFieldArray, useForm } from 'react-hook-form'
@@ -23,9 +25,10 @@ import { Fragment, use, useEffect, useMemo, useState } from 'react'
import { format } from 'date-fns'
import { ptBR } from 'react-day-picker/locale'
import { zodResolver } from '@hookform/resolvers/zod'
import Fuse from 'fuse.js'
import { formatCPF } from '@brazilian-utils/brazilian-utils'
import { pick } from 'ramda'
import { DateTime } from '@repo/ui/components/datetime'
import { Avatar, AvatarFallback } from '@repo/ui/components/ui/avatar'
import { Abbr } from '@repo/ui/components/abbr'
import {
@@ -51,6 +54,7 @@ import {
} from '@repo/ui/components/ui/input-group'
import {
Card,
CardAction,
CardContent,
CardDescription,
CardHeader,
@@ -75,7 +79,6 @@ import { cloudflareContext } from '@repo/auth/context'
import { SearchFilter } from '@repo/ui/components/search-filter'
import { formSchema, type Schema, MAX_ITEMS } from './data'
import { pick } from 'ramda'
export function meta({}: Route.MetaArgs) {
return [{ title: 'Adicionar matrícula' }]
@@ -215,6 +218,9 @@ export default function Route({
<CardDescription>
Siga os passos abaixo para adicionar novas matrículas.
</CardDescription>
<CardAction>
<ActionMenu />
</CardAction>
</CardHeader>
<CardContent className="space-y-4">
@@ -731,3 +737,72 @@ function DuplicateRowMultipleTimes({
</Popover>
)
}
function ActionMenu() {
const { orgid } = useParams()
const [open, { set }] = useToggle()
const { data, runAsync, loading } = useRequest(
async () => {
const r = await fetch(`/~/api/orgs/${orgid}/enrollments/submissions`, {
method: 'GET'
})
return await r.json()
},
{ manual: true }
)
return (
<Popover
open={open}
onOpenChange={async (open) => {
set(open)
await runAsync()
}}
>
<PopoverTrigger asChild>
<Button variant="ghost" className="cursor-pointer">
<EllipsisIcon />
</Button>
</PopoverTrigger>
<PopoverContent align="end" className="p-0 overflow-hidden w-56">
<div className="border-b p-2 text-xs text-muted-foreground font-medium">
Envios recentes
</div>
<Command className="rounded-none">
<CommandList>
<CommandGroup>
{loading && (
<CommandItem disabled>
<Spinner />
</CommandItem>
)}
{data?.items?.map(({ sk }, index) => (
<CommandItem asChild key={index}>
<Link
to={`../enrollments/${sk}/submitted`}
className="cursor-pointer"
>
<DateTime
options={{
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
}}
>
{sk}
</DateTime>
</Link>
</CommandItem>
))}
{data?.items?.length === 0 && (
<CommandEmpty>Nenhum envio ainda</CommandEmpty>
)}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
)
}

View File

@@ -406,14 +406,16 @@ function Failed({ items = [] }) {
}
function ActionMenu({ sk }: { sk: string }) {
const [open, { toggle, set }] = useToggle()
const { revalidate } = useRevalidator()
const onSuccess = () => {
revalidate()
set(false)
}
return (
<DropdownMenu>
<DropdownMenu open={open} onOpenChange={toggle}>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon-sm" className="cursor-pointer">
<EllipsisIcon />

View File

@@ -2,12 +2,15 @@ import type { Route } from './+types/route'
import * as cookie from 'cookie'
import { Outlet, type ShouldRevalidateFunctionArgs } from 'react-router'
import { useEffect } from 'react'
import { request as req } from '@repo/util/request'
import {
WorkspaceProvider,
type Workspace
} from '@/components/workspace-switcher'
import { userContext } from '@repo/auth/context'
import { Toaster } from '@repo/ui/components/ui/sonner'
import { authMiddleware } from '@repo/auth/middleware/auth'
import { ModeToggle, ThemedImage } from '@repo/ui/components/dark-mode'
import { NavUser } from '@repo/ui/components/nav-user'
@@ -16,10 +19,9 @@ import {
SidebarProvider,
SidebarTrigger
} from '@repo/ui/components/ui/sidebar'
import { Toaster } from '@repo/ui/components/ui/sonner'
import { request as req } from '@repo/util/request'
import { AppSidebar } from '@/components/app-sidebar'
// import { Notification } from '@/components/notification'
export const middleware: Route.MiddlewareFunction[] = [authMiddleware]
@@ -64,6 +66,16 @@ export function shouldRevalidate({
export default function Route({ loaderData }: Route.ComponentProps) {
const { user, orgs, sidebar_state } = loaderData
useEffect(() => {
if (typeof window !== 'undefined' && window.rybbit) {
window.rybbit.identify(user.sub, {
username: user.email,
name: user.name,
email: user.email
})
}
}, [])
return (
<WorkspaceProvider workspaces={orgs as Workspace[]}>
<SidebarProvider defaultOpen={sidebar_state === 'true'} className="flex">

View File

@@ -14,7 +14,7 @@ Globals:
Architectures:
- x86_64
Layers:
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:100
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:103
Environment:
Variables:
TZ: America/Sao_Paulo

View File

@@ -25,7 +25,7 @@ Globals:
Architectures:
- x86_64
Layers:
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:100
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:103
Environment:
Variables:
TZ: America/Sao_Paulo

View File

@@ -14,7 +14,7 @@ Globals:
Architectures:
- x86_64
Layers:
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:100
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:103
Environment:
Variables:
TZ: America/Sao_Paulo

View File

@@ -20,7 +20,7 @@ Globals:
Architectures:
- x86_64
Layers:
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:100
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:103
Environment:
Variables:
TZ: America/Sao_Paulo

View File

@@ -623,6 +623,7 @@ class DynamoDBPersistenceLayer:
expr_attr_values: dict = {},
start_key: dict = {},
filter_expr: str | None = None,
projection_expr: str | None = None,
limit: int | None = None,
index_forward: bool = True,
table_name: str | None = None,
@@ -661,6 +662,9 @@ class DynamoDBPersistenceLayer:
if filter_expr:
attrs['FilterExpression'] = filter_expr
if projection_expr:
attrs['ProjectionExpression'] = projection_expr
if limit:
attrs['Limit'] = limit
@@ -1148,6 +1152,7 @@ class DynamoDBCollection:
expr_attr_values: dict = {},
start_key: str | None = None,
filter_expr: str | None = None,
projection_expr: str | None = None,
index_forward: bool = False,
limit: int = LIMIT,
table_name: str | None = None,
@@ -1209,6 +1214,7 @@ class DynamoDBCollection:
expr_attr_name=key.expr_attr_name() | expr_attr_name,
expr_attr_values=key.expr_attr_values() | expr_attr_values,
filter_expr=filter_expr,
projection_expr=projection_expr,
index_forward=index_forward,
limit=limit,
start_key=_startkey_b64decode(start_key) if start_key else {},

View File

@@ -1,6 +1,6 @@
[project]
name = "layercake"
version = "0.11.3"
version = "0.11.4"
description = "Packages shared dependencies to optimize deployment and ensure consistency across functions."
readme = "README.md"
authors = [

View File

@@ -26,7 +26,7 @@ Globals:
Architectures:
- x86_64
Layers:
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:100
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:103
Environment:
Variables:
TZ: America/Sao_Paulo

View File

@@ -8,7 +8,7 @@ Globals:
Architectures:
- x86_64
Layers:
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:100
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:103
Environment:
Variables:
LOG_LEVEL: DEBUG
@@ -16,12 +16,12 @@ Globals:
POWERTOOLS_LOGGER_SAMPLE_RATE: 0.1
POWERTOOLS_LOGGER_LOG_EVENT: true
MEILISEARCH_HOST: https://meili.saladeaula.digital
MEILISEARCH_API_KEY: "{{resolve:ssm:/saladeaula/meili_api_key}}"
MEILISEARCH_API_KEY: '{{resolve:ssm:/saladeaula/meili_api_key}}'
POSTGRES_DB: saladeaula.digital
POSTGRES_HOST: sp-node01.saladeaula.digital
POSTGRES_PORT: 5432
POSTGRES_USER: "{{resolve:ssm:/saladeaula/postgres_user}}"
POSTGRES_PASSWORD: "{{resolve:ssm:/saladeaula/postgres_password}}"
POSTGRES_USER: '{{resolve:ssm:/saladeaula/postgres_user}}'
POSTGRES_PASSWORD: '{{resolve:ssm:/saladeaula/postgres_password}}'
Resources:
MeilisearchLog:

View File

@@ -17,7 +17,7 @@ Globals:
Architectures:
- x86_64
Layers:
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:100
- !Sub arn:aws:lambda:sa-east-1:336641857101:layer:layercake:103
Environment:
Variables:
TZ: America/Sao_Paulo