Appearance
Team API: Especificação para Backend
Contexto
O módulo Team é a visão do tomador (borrower) sobre todos os membros da sua equipe contratada: pessoas jurídicas (PJ, via Company) e pessoas físicas (PF, via User) ligadas via contract_parts. Substitui o antigo módulo borrower-providers.
A página de detalhe (/team/:id) tem:
- Sidebar lateral direita com painéis: Contrato, Documentos, Ajuste de horas, Geral, Sessões (apenas PF)
- 5 abas: Entries, Consolidated, Compliance, Invoices, History
- Switch Ativo/Inativo no header do painel "Geral" (controla
ContractPart.isActive)
:id em todas as rotas listadas pode ser interpretado como o companyId/userId do prestador (compatível com o legado de compliance) exceto no endpoint de toggle, onde é o contractPartId.
Modelo: TeamMember
Estrutura unificada PJ + PF retornada pelos endpoints. Nunca duplica dados: campos como nome/email vêm de user/company (FKs com include Prisma).
ts
type TeamMemberType = 'NATURAL_PERSON' | 'LEGAL_ENTITY'
type TeamMember = {
contractPartId: string // ID único do vínculo
type: TeamMemberType
isActive: boolean // ContractPart.isActive
userId: string // SEMPRE notnull
companyId: string | null // null quando PF; preenchido quando PJ
user: { // sempre presente (PJ extrai do owner da company)
id: string
name: string
email: string
phone: string | null
avatar: string | null
cpf: string | null
}
company: { // null quando PF
id: string
name: string
tradeName: string | null
document: string
email: string | null
phone: string | null
logo: string | null
} | null
contractId?: string
contractName?: string
totalHoursMonth: number
totalHoursWeek: number
lastEntry?: string | null // ISO
hasActiveContract?: boolean // só na listagem
}Regra do user notnull:
- PF (
ContractPart.userId != null):user= User do contrato;company = null - PJ (
ContractPart.companyId != null):user= User responsável (OWNER da Company, via includecompany.usersfiltrado poruserCompanyRole = OWNER);company= a Company
Endpoints
GET /team
Lista todos os membros do tomador com resumo de horas (PJ + PF mesclados).
Auth: Bearer token, role borrower
Query Params:
| Param | Tipo | Obrigatório | Descrição |
|---|---|---|---|
| search | string | Não | Busca textual |
| status | "active" | "inactive" | Não | Filtrar por status |
| page | number | Não | Paginação |
| pageSize | number | Não | Paginação |
A busca cobre simultaneamente: company.name, company.tradeName, company.document, user.name, user.email, user.cpf.
Response 200: { data: TeamMember[], meta: { total, page, pageSize, totalPages } }
GET /team/:id
Retorna o detalhe completo de um membro: dados cadastrais (member), contrato ativo, histórico, documentos e sessões.
Auth: Bearer token, role borrower
Path Params:
| Param | Tipo | Descrição |
|---|---|---|
| id | string (UUID) | companyId (PJ) ou userId (PF) do membro |
Response 200:
json
{
"member": TeamMember,
"contract": TeamMemberContract | null,
"contractsHistory": TeamMemberContractHistory[],
"documents": TeamMemberDocument[],
"sessions": SessionEntry[]
}| Chave | Descrição |
|---|---|
member | Estrutura TeamMember (acima) |
contract | Contrato ativo (mesmos campos do antigo BorrowerProviderContract) ou null |
contractsHistory | Contratos encerrados ({ ...contract, isActive: false }) |
documents | Documentos do contrato ativo (pending / approved / rejected) |
sessions | Sessions do User (browser, OS, IP, createdAt). Vazio para PJ |
Response 404: Membro não encontrado
PATCH /team/:contractPartId/active
Ativa ou inativa o vínculo do membro com a empresa. Atualiza somente ContractPart.isActive: não toca em User.status nem Company.
Auth: Bearer token, role borrower
Path Params:
| Param | Tipo | Descrição |
|---|---|---|
| contractPartId | string (UUID) | ID do ContractPart |
Body:
json
{ "isActive": true }| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
| isActive | boolean | Sim | Novo estado do vínculo |
Response 200: { data: TeamMember }
Response 404: ContractPart não encontrado para o tomador atual
POST /team
Cria membership (vínculo de Company + User responsável) na equipe do tomador. Não cria ContractPart: o ContractPart é criado quando esse membro é incluído num contrato pelo módulo de Contratos.
Auth: Bearer token, role borrower
Body:
json
{
"document": "12345678000190",
"name": "Tech Solutions Ltda",
"tradeName": "TechSol",
"legalRepresentative": {
"cpf": "12345678900",
"name": "João Silva",
"email": "joao@techsol.com.br"
}
}Response 201:
json
{
"user": { "id": "...", "name": "...", "email": "..." },
"company": { "id": "...", "name": "...", "document": "..." }
}Erros:
400: empresa = borrower (não pode ser membro de si mesma)400: representante legal é o próprio usuário atual400: membro já vinculado
PUT /team/:id
Mantido por compatibilidade, não atualiza nada (no-op que retorna o membro). Usar PATCH /team/:contractPartId/active para alterar status.
DELETE /team/:id
Remove o vínculo (deleta o ContractPart) entre o membro e a empresa.
Auth: Bearer token, role borrower
Response 204: No Content
Endpoints de Compliance do Membro
Os endpoints de compliance mantêm :providerId na rota (que é o companyId ou userId do membro). A entidade Prisma ProviderComplianceFlow permanece com nome legado.
GET /team/:providerId/compliances
Lista compliances mensais do membro (derivados dos fluxos anexados).
Query Params:
| Param | Tipo | Obrigatório | Descrição |
|---|---|---|---|
| year | number | Não | Filtrar por ano |
| status | ComplianceStatus | "all" | Não | Filtrar por status |
Response 200: { data: ProviderCompliance[] }
PATCH /team/:providerId/compliances/:complianceId/steps/:stepId/review
Aprova ou rejeita um documento enviado em um step.
Body:
json
{
"documentId": "uuid",
"action": "approve",
"rejectionReason": null
}| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
| documentId | string (UUID) | Sim | ID do StepComplianceDocument |
| action | "approve" | "reject" | Sim | Ação |
| rejectionReason | string | Condicional | Obrigatório quando action = "reject" |
Regras:
- Se todos documentos
approved→ step status =approved - Se algum documento
rejected→ step status =rejected - Ao aprovar um step, desbloqueia o próximo (
dependsOnPrevious) - Recalcula o status geral do compliance
Response 200: { data: StepComplianceDocument }
GET /team/:providerId/compliance-flows
Retorna todos os fluxos de compliance anexados ao membro.
Response 200: { data: ProviderComplianceFlow[] }
POST /team/compliance-flows
Anexa fluxos de compliance a múltiplos membros em uma chamada.
Body:
json
{
"attachments": [
{ "providerId": "uuid", "complianceFlowId": "uuid" }
]
}POST /team/:providerId/compliance-flows
Anexa um fluxo de compliance ao membro com chefias por step opcionais.
Body:
json
{
"complianceFlowId": "flow-uuid",
"stepLeaderships": [
{ "stepId": "step-uuid", "immediateLeadershipIds": ["user-uuid"] }
]
}Response 409: Fluxo já anexado ao membro
PUT /team/:providerId/compliance-flows/:flowId
Atualiza chefias imediatas por step de um fluxo já anexado.
Body:
json
{
"stepLeaderships": [
{ "stepId": "step-uuid", "immediateLeadershipIds": ["user-uuid"] }
]
}Na UI, o botão "Editar" no header do fluxo de compliance do prestador abre o drawer de edição de chefias imediatas por step e salva por este endpoint. O :flowId é o providerComplianceFlowId que vem no objeto de compliance (ProviderCompliance.providerComplianceFlowId), o identificador do anexo.
DELETE /team/:providerId/compliance-flows/:flowId
Desanexa o fluxo do membro. Na UI, o botão "Desanexar" no header do fluxo dispara este endpoint. O :flowId também é o providerComplianceFlowId do objeto de compliance.
Cliente vs. prestador: o módulo Clientes reutiliza o mesmo
DELETE(viateamService.removeComplianceFlow) para desanexar, mas não expõe oPUTde chefias: o fluxo anexado a um cliente não usa lideranças imediatas por etapa, então o header do fluxo do cliente oferece apenas "Remover".
Endpoints de Horas (Separados)
Mantidos sob /monitoring/... (sem alteração):
| Método | Endpoint | Descrição |
|---|---|---|
| GET | /monitoring/:id | Detalhe de horas (summaries diário, semanal, mensal) |
| GET | /monitoring/:id/entries | Lista entradas de horas (filtros: startDate, endDate) |
| POST | /monitoring/entries | Cria entrada de horas |
| PUT | /monitoring/entries/:id | Atualiza entrada de horas |
| DELETE | /monitoring/entries/:id | Exclui entrada de horas |
Schema Prisma: Mudanças
ContractPart
Adicionada coluna isActive:
prisma
model ContractPart {
id String @id @default(uuid()) @db.Uuid
contractId String @db.Uuid
companyId String? @db.Uuid
userId String? @db.Uuid
isActive Boolean @default(true)
// ...
}A invariante companyId XOR userId permanece. A unificação acontece só na resposta dos endpoints de team (via mapper): o schema continua representando PJ e PF em colunas separadas.
Migration: 20260509200900_contract_part_is_active.
Tipos TypeScript de Referência
ts
type TeamMemberDetailData = {
member: TeamMember
contract: TeamMemberContract | null
contractsHistory: TeamMemberContractHistory[]
documents: TeamMemberDocument[]
sessions: SessionEntry[]
}
type TeamMemberContract = {
id: string
name: string
status: string
startDate: string
endDate: string
contractType?: string
partRateValue?: number
monthlyHours?: number
value?: number
description?: string
signedDocument?: string
createdAt: string
updatedAt: string
}
type TeamMemberContractHistory = TeamMemberContract & { isActive: boolean }
type TeamMemberDocument = {
id: string
name: string
fileName: string
fileUrl: string
fileSize?: number
status: 'pending' | 'approved' | 'rejected'
uploadedAt: string
}
type SessionEntry = {
id: string
browser: string
os: string
ip: string
createdAt: string
}Funcionalidades Futuras
- Filtro por tipo (PJ/PF) na listagem
- Bulk toggle Ativo/Inativo para múltiplos membros
- Sessões para PJ: atualmente vazio, futuramente podemos exibir sessões dos
usersda Company