Skip to content

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 include company.users filtrado por userCompanyRole = 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:

ParamTipoObrigatórioDescrição
searchstringNãoBusca textual
status"active" | "inactive"NãoFiltrar por status
pagenumberNãoPaginação
pageSizenumberNãoPaginaçã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:

ParamTipoDescrição
idstring (UUID)companyId (PJ) ou userId (PF) do membro

Response 200:

json
{
  "member": TeamMember,
  "contract": TeamMemberContract | null,
  "contractsHistory": TeamMemberContractHistory[],
  "documents": TeamMemberDocument[],
  "sessions": SessionEntry[]
}
ChaveDescrição
memberEstrutura TeamMember (acima)
contractContrato ativo (mesmos campos do antigo BorrowerProviderContract) ou null
contractsHistoryContratos encerrados ({ ...contract, isActive: false })
documentsDocumentos do contrato ativo (pending / approved / rejected)
sessionsSessions 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:

ParamTipoDescrição
contractPartIdstring (UUID)ID do ContractPart

Body:

json
{ "isActive": true }
CampoTipoObrigatórioDescrição
isActivebooleanSimNovo 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 atual
  • 400: 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:

ParamTipoObrigatórioDescrição
yearnumberNãoFiltrar por ano
statusComplianceStatus | "all"NãoFiltrar 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
}
CampoTipoObrigatórioDescrição
documentIdstring (UUID)SimID do StepComplianceDocument
action"approve" | "reject"SimAção
rejectionReasonstringCondicionalObrigató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 (via teamService.removeComplianceFlow) para desanexar, mas não expõe o PUT de 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étodoEndpointDescrição
GET/monitoring/:idDetalhe de horas (summaries diário, semanal, mensal)
GET/monitoring/:id/entriesLista entradas de horas (filtros: startDate, endDate)
POST/monitoring/entriesCria entrada de horas
PUT/monitoring/entries/:idAtualiza entrada de horas
DELETE/monitoring/entries/:idExclui 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

  1. Filtro por tipo (PJ/PF) na listagem
  2. Bulk toggle Ativo/Inativo para múltiplos membros
  3. Sessões para PJ: atualmente vazio, futuramente podemos exibir sessões dos users da Company