diff --git a/api.saladeaula.digital/app/routes/orgs/users/__init__.py b/api.saladeaula.digital/app/routes/orgs/users/__init__.py index f5e79fd..8d48286 100644 --- a/api.saladeaula.digital/app/routes/orgs/users/__init__.py +++ b/api.saladeaula.digital/app/routes/orgs/users/__init__.py @@ -51,7 +51,7 @@ class OrgMissingError(NotFoundError): ... @router.post('//users') -def add_user( +def add( org_id: str, user: Annotated[User, Body(embed=True)], org: Annotated[Org, Body(embed=True)], @@ -67,6 +67,34 @@ def add_user( return JSONResponse(HTTPStatus.NO_CONTENT) +@router.delete('//users/') +def unlink(org_id: str, user_id: str): + with dyn.transact_writer() as transact: + transact.delete( + key=KeyPair( + pk=f'orgmembers#{org_id}', + # Post-migration: uncomment the following line + # pk=f'MEMBER#ORG#{org_id}', + sk=user_id, + ) + ) + transact.delete( + key=KeyPair( + pk=user_id, + sk=f'orgs#{org_id}', + # Post-migration: uncomment the following line + # pk=f'ORG#{org_id}', + ) + ) + transact.update( + key=KeyPair(user_id, '0'), + update_expr='DELETE tenant_id :org_id', + expr_attr_values={':org_id': {org_id}}, + ) + + return JSONResponse(HTTPStatus.NO_CONTENT) + + def _create_user(user: User, org: Org) -> bool: now_ = now() user_id = uuid4() @@ -79,7 +107,9 @@ def _create_user(user: User, org: Org) -> bool: 'id': user_id, 'sk': '0', 'email_verified': False, - 'org_id': {org.id}, + 'tenant_id': {org.id}, + # Post-migration: uncomment the folloing line + # 'org_id': {org.id}, 'created_at': now_, }, ) @@ -116,8 +146,9 @@ def _create_user(user: User, org: Org) -> bool: transact.put( item={ 'id': user_id, - # Post-migration: rename `orgs` to `ORG` 'sk': f'orgs#{org.id}', + # Post-migration: uncomment the following line + # pk=f'ORG#{org.id}', 'name': org.name, 'cnpj': org.cnpj, 'created_at': now_, @@ -125,8 +156,9 @@ def _create_user(user: User, org: Org) -> bool: ) transact.put( item={ - # Post-migration: rename `orgmembers` to `ORGMEMBER` 'id': f'orgmembers#{org.id}', + # Post-migration: uncomment the following line + # pk=f'MEMBER#ORG#{org_id}', 'sk': user_id, 'created_at': now_, } @@ -148,10 +180,14 @@ def _add_member(user_id: str, org: Org) -> None: with dyn.transact_writer() as transact: transact.update( key=KeyPair(user_id, '0'), - update_expr='ADD org_id :org_id', + update_expr='ADD tenant_id :org_id', + # Post-migration: uncomment the following line + # update_expr='ADD tenant_id :org_id', expr_attr_values={ ':org_id': {org.id}, }, + cond_expr='attribute_exists(sk)', + exc_cls=UserMissingError, ) transact.put( item={ diff --git a/api.saladeaula.digital/tests/routes/orgs/test_users.py b/api.saladeaula.digital/tests/routes/orgs/test_users.py index 42c11ac..3ba4310 100644 --- a/api.saladeaula.digital/tests/routes/orgs/test_users.py +++ b/api.saladeaula.digital/tests/routes/orgs/test_users.py @@ -3,6 +3,7 @@ from http import HTTPMethod, HTTPStatus from layercake.dynamodb import ( DynamoDBPersistenceLayer, + KeyPair, PartitionKey, SortKey, TransactKey, @@ -50,7 +51,8 @@ def test_add_user( assert 'email' in user assert 'email_verified' in user assert 'created_at' in user - assert 'org_id' in user + # assert 'org_id' in user + assert 'tenant_id' in user assert 'emails#scott@stonetemplopilots.com' in user @@ -149,3 +151,30 @@ def test_org_not_found( ) body = json.loads(r['body']) assert body['type'] == 'OrgMissingError' + + +def test_unlink( + app, + seeds, + http_api_proxy: HttpApiProxy, + dynamodb_persistence_layer: DynamoDBPersistenceLayer, + lambda_context: LambdaContext, +): + r = app.lambda_handler( + http_api_proxy( + raw_path='/orgs/f6000f79-6e5c-49a0-952f-3bda330ef278/users/15bacf02-1535-4bee-9022-19d106fd7518', + method=HTTPMethod.DELETE, + ), + lambda_context, + ) + assert r['statusCode'] == HTTPStatus.NO_CONTENT + + members = dynamodb_persistence_layer.collection.query( + PartitionKey('orgmembers#f6000f79-6e5c-49a0-952f-3bda330ef278') + ) + assert len(members['items']) == 0 + + orgs = dynamodb_persistence_layer.collection.query( + KeyPair('15bacf02-1535-4bee-9022-19d106fd7518', 'orgs#') + ) + assert len(orgs['items']) == 0 diff --git a/api.saladeaula.digital/tests/seeds.jsonl b/api.saladeaula.digital/tests/seeds.jsonl index 990448b..f2efdd4 100644 --- a/api.saladeaula.digital/tests/seeds.jsonl +++ b/api.saladeaula.digital/tests/seeds.jsonl @@ -1,10 +1,11 @@ // Users +{"id": "213a6682-2c59-4404-9189-12eec0a846d4", "sk": "orgs#f6000f79-6e5c-49a0-952f-3bda330ef278", "name": "Banco do Brasil", "cnpj": "00000000000191"} {"id": "15bacf02-1535-4bee-9022-19d106fd7518", "sk": "0", "name": "Sérgio R Siqueira", "email": "sergio@somosbeta.com.br", "cpf": "07879819908"} {"id": "15bacf02-1535-4bee-9022-19d106fd7518", "sk": "emails#sergio@somosbeta.com.br", "email_primary": true, "mx_record_exists": true} -{"id": "213a6682-2c59-4404-9189-12eec0a846d4", "sk": "orgs#f6000f79-6e5c-49a0-952f-3bda330ef278", "name": "Banco do Brasil", "cnpj": "00000000000191"} // User orgs -{"id": "15bacf02-1535-4bee-9022-19d106fd7518", "sk": "orgs#286f7729-7765-482a-880a-0b153ea799be", "name": "Banco do Brasil", "cnpj": "00000000000191"} +{"id": "15bacf02-1535-4bee-9022-19d106fd7518", "sk": "orgs#f6000f79-6e5c-49a0-952f-3bda330ef278", "name": "Banco do Brasil", "cnpj": "00000000000191"} + // Enrollments {"id": "578ec87f-94c7-4840-8780-bb4839cc7e64", "sk": "0", "course": {"id": "af3258f0-bccf-4781-aec6-d4c618d234a7", "name": "pytest", "access_period": 180}, "user": {"id": "068b4600-cc36-4b55-b832-bb620021705a", "name": "Benjamin Burnley", "email": "burnley@breakingbenjamin.com"}} diff --git a/apps/admin.saladeaula.digital/app/components/data-table/data-table.tsx b/apps/admin.saladeaula.digital/app/components/data-table/data-table.tsx index 79dbca3..daeedbc 100644 --- a/apps/admin.saladeaula.digital/app/components/data-table/data-table.tsx +++ b/apps/admin.saladeaula.digital/app/components/data-table/data-table.tsx @@ -44,22 +44,26 @@ interface DataTableProps { hiddenColumn?: string[] } -const TableContext = createContext<{ table: Table } | null>(null) +interface TableContextProps { + table: Table +} + +const TableContext = createContext | null>(null) export function useDataTable() { - const ctx = useContext(TableContext) as { table: Table } | null + const ctx = useContext(TableContext) if (!ctx) { throw new Error('TableContext is null') } - return ctx + return ctx as { table: Table } } export function DataTable({ + data, children, columns, - data, sort, pageIndex, pageSize, @@ -67,10 +71,10 @@ export function DataTable({ setSelectedRows, hiddenColumn = [] }: DataTableProps) { + const [dataTable, setDataTable] = useState(data) const columnVisibilityInit = Object.fromEntries( hiddenColumn.map((column) => [column, false]) ) - const [searchParams, setSearchParams] = useSearchParams() const [columnVisibility, setColumnVisibility] = useState(columnVisibilityInit) @@ -110,8 +114,12 @@ export function DataTable({ }) } + useEffect(() => { + setDataTable(data) + }, [data]) + const table = useReactTable({ - data, + data: dataTable, columns, rowCount, state: { @@ -131,7 +139,12 @@ export function DataTable({ onRowSelectionChange: setRowSelection, onSortingChange: setSorting, onColumnVisibilityChange: setColumnVisibility, - onPaginationChange: setPagination + onPaginationChange: setPagination, + meta: { + removeRow: (rowId: string) => { + setDataTable((rows) => rows.filter((row: any) => row?.id !== rowId)) + } + } }) useEffect(() => { @@ -159,6 +172,7 @@ export function DataTable({ key={header.id} className={cn( 'p-2.5', + // @ts-ignore header.column.columnDef.meta?.className )} > @@ -187,6 +201,7 @@ export function DataTable({ key={cell.id} className={cn( 'p-2.5', + // @ts-ignore cell.column.columnDef.meta?.className )} > diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.certs._index/route.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.certs._index/route.tsx index 8b1dcf0..a69ac8e 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.certs._index/route.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.certs._index/route.tsx @@ -3,5 +3,17 @@ export function meta({}) { } export default function Route() { - return <>index org + return ( + <> +
+

+ Gerenciar certificações +

+

+ Centralize o controle das certificações dos colaboradores e acompanhe + prazos e renovações com facilidade. +

+
+ + ) } 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 e745edc..976c9a4 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 @@ -2,14 +2,17 @@ import { formatCPF } from '@brazilian-utils/brazilian-utils' import { type ColumnDef } from '@tanstack/react-table' +import { useToggle } from 'ahooks' import { EllipsisVerticalIcon, PencilIcon, UserRoundMinusIcon } from 'lucide-react' -import { NavLink } from 'react-router' +import { NavLink, useParams } from 'react-router' +import { toast } from 'sonner' import { Abbr } from '@/components/abbr' +import { useDataTable } from '@/components/data-table/data-table' import { Avatar, AvatarFallback } from '@repo/ui/components/ui/avatar' import { Button } from '@repo/ui/components/ui/button' import { @@ -123,12 +126,39 @@ export const columns: ColumnDef[] = [ )} - - Desvincular - + ) } ] + +function UnlinkMenuItem({ userId }: { userId: string }) { + const [loading, { set }] = useToggle(false) + const { orgid } = useParams() + const { table } = useDataTable() + + return ( + { + e.preventDefault() + set(true) + + const r = await fetch(`/~/api/orgs/${orgid}/users/${userId}`, { + method: 'DELETE' + }) + + if (r.ok) { + toast.info('O colaborador foi desvinculado') + // @ts-ignore + table.options.meta?.removeRow?.(userId) + } + }} + > + {loading ? : } Desvincular + + ) +} diff --git a/apps/admin.saladeaula.digital/app/routes/_.$orgid.users.add/route.tsx b/apps/admin.saladeaula.digital/app/routes/_.$orgid.users.add/route.tsx index 2618f78..ae8ac75 100644 --- a/apps/admin.saladeaula.digital/app/routes/_.$orgid.users.add/route.tsx +++ b/apps/admin.saladeaula.digital/app/routes/_.$orgid.users.add/route.tsx @@ -73,8 +73,6 @@ export async function action({ params, request, context }: Route.ActionArgs) { context }) - console.log(r) - if (!r.ok) { const error = await r.json().catch(() => ({})) return { ok: false, error } diff --git a/apps/admin.saladeaula.digital/tsconfig.json b/apps/admin.saladeaula.digital/tsconfig.json index bbf6a6a..bf56094 100644 --- a/apps/admin.saladeaula.digital/tsconfig.json +++ b/apps/admin.saladeaula.digital/tsconfig.json @@ -4,6 +4,7 @@ { "path": "./tsconfig.node.json" }, { "path": "./tsconfig.cloudflare.json" } ], + "include": ["types/**/*.d.ts"], "compilerOptions": { "checkJs": true, "verbatimModuleSyntax": true, diff --git a/apps/admin.saladeaula.digital/types/react-table.d.ts b/apps/admin.saladeaula.digital/types/react-table.d.ts new file mode 100644 index 0000000..a9df67f --- /dev/null +++ b/apps/admin.saladeaula.digital/types/react-table.d.ts @@ -0,0 +1,11 @@ +import '@tanstack/react-table' + +declare module '@tanstack/react-table' { + interface ColumnMeta { + className?: string + } + + interface TableMeta { + removeRow?: (rowId: string) => void + } +}