Skip to content

Ponto & Apontamentos ​

Este é o núcleo do módulo. O fluxo funciona assim: o profissional bate o ponto (punches/in → punches/out), a aplicação cria o apontamento (entry) e o dia (day) correspondente, consolida os totais do dia a cada fechamento e congela o snapshot financeiro. Correções manuais passam por entries e geram revisões de auditoria.

Dois princípios estruturais:

  • Punches são fatos, entries são interpretação. A batida bruta (timesheet_punches) nunca sofre UPDATE — guarda instante, coordenada, dispositivo e IP. O apontamento (timesheet_entries) pode ser corrigido, e cada correção vira uma revisão com antes/depois.
  • Ninguém varre apontamento para montar tela. O dia consolidado (timesheet_days) mantém os totais e o status do fluxo de aprovação — é ele que dashboards e relatórios leem.

Headers obrigatórios

Todas as requisições devem incluir X-Timesheet-Id e Authorization: Bearer {token}. A batida de ponto é pessoal (sempre do autenticado); gestão de locais e revisões exige gestor.


Locais (Locations) ​

Locais autorizados com raio — as cercas virtuais (geofences). Quando a instância tem isRequiresGeolocation: true, toda batida precisa vir com coordenadas dentro do raio de algum local ativo.

Estrutura de Dados ​

CampoTipoDescrição
nameStringNome do local (máx. 150 caracteres).
addressStringEndereço (máx. 250 caracteres, opcional).
latitude / longitudeNumberCoordenadas do centro da cerca.
radiusMetersNumberRaio em metros (inteiro, mín. 1).
isActiveBooleanSomente locais ativos participam da validação da batida.

Endpoints ​

http
GET    /v1/timesheet/locations
GET    /v1/timesheet/locations/list
GET    /v1/timesheet/locations/:id
POST   /v1/timesheet/locations
PUT    /v1/timesheet/locations/:id
DELETE /v1/timesheet/locations/:id

Listagens são abertas a qualquer profissional (a listagem aceita isActive como filtro); escrita exige gestor.


Batidas de Ponto (Punches) ​

A batida abre ou fecha um apontamento do profissional autenticado. O punchedAt é opcional — omitido, vale o instante do servidor. O dia local é resolvido pelo fuso do profissional (ou o da instância), e o dia consolidado é criado automaticamente na primeira batida.

Estrutura de Dados (punch) ​

CampoTipoDescrição
kindStringin ou out.
sourceStringOrigem: web (padrão), mobile, kiosk, api, import ou manual.
punchedAtDateTimeInstante da batida.
latitude / longitude / accuracyMetersNumberCoordenadas e precisão (opcionais).
addressStringEndereço resolvido (opcional).
timesheetLocationIdNumberLocal mais próximo (preenchido pela aplicação).
isWithinGeofence / distanceMetersBoolean / NumberResultado da validação da geofence (preenchido pela aplicação).
deviceId / deviceModelStringIdentificação do aparelho (opcional).
ipAddressStringIP de origem (capturado pela aplicação).
photoUrlStringFoto da batida (URL, opcional).

Bater Entrada ​

Abre um apontamento. Falha se já houver um apontamento em aberto (OPEN_ENTRY_EXISTS) ou se o dia já tiver atingido o limite de intervalos da instância (maxIntervalsPerDay, MAX_INTERVALS_REACHED).

http
POST /v1/timesheet/punches/in

Payload (Body) ​

AtributoTipoObrigatórioDescrição
punchedAtdatetimeNãoInstante da batida (ISO 8601 ou YYYY-MM-DD HH:mm:ss; padrão: agora).
kindstringNãoTipo do apontamento: work (padrão), break, travel, on_call ou training.
sourcestringNãoOrigem (padrão: web).
latitude / longitudenumberNãoCoordenadas da batida.
accuracyMetersnumberNãoPrecisão em metros.
addressstringNãoEndereço (máx. 250 caracteres).
deviceId / deviceModelstringNãoIdentificação do aparelho (máx. 150 caracteres).
photoUrlstringNãoURL da foto da batida.

Respostas ​

201 Created

Retorna o ID do apontamento aberto: { "id": 1 }.

422 Unprocessable Entity

Regras de negócio da batida — ver códigos de erro.

Bater Saída ​

Fecha o apontamento aberto, calcula durationMinutes, recalcula os totais do dia e congela o snapshot financeiro.

http
POST /v1/timesheet/punches/out

Mesmo payload da entrada (sem kind). A saída deve ser posterior à entrada e o apontamento não pode ultrapassar 24 horas — ambos os casos falham com INVALID_PUNCH_INTERVAL.

Respostas ​

200 OK

Retorna o apontamento fechado.

422 Unprocessable Entity

Não há apontamento aberto (NO_OPEN_ENTRY) ou intervalo inválido (INVALID_PUNCH_INTERVAL).

Apontamento Atual ​

Retorna o apontamento em aberto do profissional autenticado, ou null se não houver.

http
GET /v1/timesheet/punches/current

Validação de Geolocalização ​

Quando a batida vem com coordenadas, a aplicação calcula a distância (haversine) até cada local ativo e registra o mais próximo, com isWithinGeofence e distanceMeters. Quando a instância tem isRequiresGeolocation: true:

SituaçãoErro
Batida sem coordenadas422 GEOLOCATION_REQUIRED
Nenhum local ativo cadastrado422 OUTSIDE_GEOFENCE
Fora do raio de todos os locais422 OUTSIDE_GEOFENCE

Sem isRequiresGeolocation, as coordenadas são apenas registradas — batidas fora da cerca são aceitas e marcadas com isWithinGeofence: false.

Códigos de Erro do Ponto ​

CodeStatusSignificado
OPEN_ENTRY_EXISTS422Já existe um apontamento em aberto.
NO_OPEN_ENTRY422Saída sem apontamento aberto.
MAX_INTERVALS_REACHED422Limite diário de intervalos atingido.
INVALID_PUNCH_INTERVAL422Saída anterior à entrada ou apontamento com mais de 24h.
GEOLOCATION_REQUIRED422Coordenadas obrigatórias ausentes.
OUTSIDE_GEOFENCE422Fora da área permitida (ou sem locais ativos).
DAY_LOCKED422O dia está approved ou locked.
PERIOD_CLOSED422A data pertence a um período já fechado.

Dias (Days) ​

O dia local do profissional: uma linha por pessoa por data, com os totais consolidados e o status do fluxo de aprovação. O dia nasce automaticamente na primeira batida ou apontamento, já com expectedMinutes calculado a partir da jornada vigente — feriados e ausências aprovadas zeram a expectativa.

Estrutura de Dados ​

CampoTipoDescrição
workDateDateData local do dia (YYYY-MM-DD).
timezoneStringFuso usado para resolver a data.
statusStringopen, submitted, approved, rejected ou locked.
expectedMinutesNumberMinutos esperados (jornada; 0 em feriado/ausência).
workedMinutesNumberMinutos trabalhados (soma dos apontamentos fechados, exceto break).
breakMinutesNumberMinutos de intervalo (apontamentos break).
balanceMinutesNumberSaldo: workedMinutes - expectedMinutes.
isHolidayBooleanO dia é feriado não útil.
notesStringObservações (opcional).
submittedAt / approvedAt / lockedAtDateTimeMarcos do fluxo de aprovação.
approvedByTimesheetUserIdNumberGestor que aprovou.

Fluxo de Aprovação do Dia ​

open ──submit──> submitted ──approve──> approved ──lock──> locked
  ^                  │
  └─────reject───────┘
  • submit: o próprio profissional (ou um gestor) envia o dia para aprovação — vale para open e rejected.
  • approve / reject / lock: somente gestores. reject devolve o dia para correção.
  • Transição fora da tabela falha com 422 INVALID_STAGE_MOVE.

Dias approved ou locked não aceitam novas batidas nem apontamentos (DAY_LOCKED). O fechamento de um período trava (locked) todos os dias da janela.

Endpoints ​

http
GET   /v1/timesheet/me/days
GET   /v1/timesheet/days
GET   /v1/timesheet/days/:id
PATCH /v1/timesheet/days/:id/submit
PATCH /v1/timesheet/days/:id/approve
PATCH /v1/timesheet/days/:id/reject
PATCH /v1/timesheet/days/:id/lock
  • GET /me/days: os dias do próprio profissional (filtros status, startsOn/endsOn).
  • GET /days: todos os dias da instância (somente gestor), com filtro adicional timesheetUserId.
  • GET /days/:id: retorna o dia com os apontamentos (timesheetEntries) ordenados por sequence. Profissionais só enxergam os próprios dias (404 para terceiros).

Exemplo de Resposta (show) ​

json
{
  "id": 5,
  "timesheetId": 4,
  "timesheetUserId": 5,
  "workDate": "2026-09-24",
  "timezone": "America/Sao_Paulo",
  "status": "open",
  "expectedMinutes": 0,
  "workedMinutes": 150,
  "breakMinutes": 0,
  "balanceMinutes": 150,
  "isHoliday": false,
  "isDayOff": false,
  "notes": null,
  "submittedAt": null,
  "approvedByTimesheetUserId": null,
  "approvedAt": null,
  "lockedAt": null,
  "createdAt": "2026-09-25T12:33:54.319-03:00",
  "updatedAt": "2026-09-25T12:33:55.010-03:00",
  "timesheetEntries": [
    {
      "id": 6,
      "timesheetDayId": 5,
      "timesheetUserId": 5,
      "timesheetProjectId": 3,
      "timesheetTaskId": 3,
      "sequence": 1,
      "kind": "work",
      "source": "manual",
      "startedAt": "2026-09-24T09:00:00.000-03:00",
      "endedAt": "2026-09-24T11:30:00.000-03:00",
      "description": "Desenvolvimento do módulo de autenticação",
      "durationMinutes": 150,
      "isBillable": true,
      "costRate": null,
      "billingRate": "160.0000",
      "rateMultiplier": "1.0000",
      "currency": "BRL",
      "costAmount": null,
      "billingAmount": "400.00"
    }
  ]
}

Campos decimais

Os campos financeiros do apontamento (costRate, billingRate, rateMultiplier, costAmount, billingAmount) são decimais serializados como string.


Apontamentos (Entries) ​

Cada par entrada/saída. Além dos criados pela batida, apontamentos podem ser lançados manualmente (quando a instância permite) e corrigidos — toda escrita gera uma revisão de auditoria.

Estrutura de Dados ​

CampoTipoDescrição
timesheetDayIdNumberDia consolidado ao qual o apontamento pertence.
timesheetUserIdNumberProfissional dono do apontamento.
timesheetProjectId / timesheetTaskIdNumberProjeto/tarefa vinculados (opcionais) — ver Clientes, Projetos & Tarefas.
sequenceNumberOrdem do apontamento no dia.
kindStringwork, break, travel, on_call ou training.
sourceStringweb, mobile, kiosk, api, import ou manual.
startedAt / endedAtDateTimeInício e fim (endedAt: null = apontamento aberto).
durationMinutesNumberDuração calculada (minutos).
descriptionStringDescrição livre (opcional).
isBillableBooleanIndica se a hora é faturável.
costRate / billingRate / rateMultiplierNumberSnapshot financeiro congelado — ver abaixo.
currencyStringMoeda do snapshot.
costAmount / billingAmountNumberCusto e faturamento calculados.
editReasonStringMotivo da última correção manual.

Snapshot Financeiro ​

Ao fechar um apontamento, a aplicação congela:

  1. Tarifa vigente do profissional na data do dia (hourlyCost, hourlyBillingRate).
  2. Multiplicador: feriado (holidayMultiplier) tem prioridade; sábado/domingo usam weekendMultiplier; demais dias, 1.
  3. Valor-hora de venda com precedência: alocação do projeto (project_users.hourlyBillingRate) → valor-hora do projeto → tarifa do profissional.
  4. Supressão de faturamento: isBillable: false ou projeto non_billable/fixed_price zera o faturamento (o custo continua).

Apontamentos reabertos ou em aberto têm o snapshot limpo e recalculado no próximo fechamento.

Listar Apontamentos ​

http
GET /v1/timesheet/entries
GET /v1/timesheet/me/entries
GET /v1/timesheet/entries/:id
  • GET /entries: todos os apontamentos (somente gestor), com filtros timesheetUserId, timesheetProjectId, kind e startsOn/endsOn (ISO 8601).
  • GET /me/entries: os do próprio profissional.
  • GET /entries/:id: retorna o apontamento completo, incluindo as batidas vinculadas (timesheetPunches). Profissionais só enxergam os próprios (404 para terceiros).

Criar Apontamento Manual ​

Lançamento retroativo ou em nome de outro profissional (gestor). Exige isAllowsManualEntry: true na instância (MANUAL_ENTRY_NOT_ALLOWED caso contrário) e um motivo de auditoria.

http
POST /v1/timesheet/entries

Payload (Body) ​

AtributoTipoObrigatórioDescrição
timesheetUserIdnumberNãoProfissional (padrão: o autenticado; para terceiros, exige gestor — 403 FORBIDDEN).
timesheetProjectIdnumberNãoProjeto existente na instância.
timesheetTaskIdnumberNãoTarefa existente na instância.
kindstringSimwork, break, travel, on_call ou training.
startedAtdatetimeSimInício (ISO 8601).
endedAtdatetimeNãoFim (posterior a startedAt, máx. 24h; null = aberto).
descriptionstringNãoDescrição livre.
editReasonstringSimMotivo do lançamento (mín. 3 caracteres) — gravado na revisão.

Validações de Negócio ​

SituaçãoErro
Instância não permite apontamento manual422 MANUAL_ENTRY_NOT_ALLOWED
Já existe apontamento aberto para o profissional422 OPEN_ENTRY_EXISTS
Sobreposição com outro apontamento422 ENTRY_OVERLAP
Fim anterior ao início ou duração > 24h422 INVALID_DATE_RANGE
Data em período fechado422 PERIOD_CLOSED

Respostas ​

201 Created

Retorna o ID do apontamento: { "id": 1 }. O dia é recalculado automaticamente.

Corrigir Apontamento ​

Edita horários, projeto, tarefa, tipo ou descrição. A edição pode mover o apontamento para outro dia (o dia de origem e o de destino são recalculados).

http
PUT /v1/timesheet/entries/:id

Mesmos campos da criação (todos opcionais, exceto editReason, sempre obrigatório). O dia precisa estar editável: para gestores, approved/locked bloqueiam; para profissionais, qualquer status diferente de open bloqueia (DAY_LOCKED).

Respostas ​

204 No Content

Correção concluída. Uma revisão update é gravada com antes/depois, autor e motivo.

Remover Apontamento ​

Soft delete com motivo obrigatório (editReason, mín. 3 caracteres) — gera uma revisão delete.

http
DELETE /v1/timesheet/entries/:id

Respostas ​

204 No Content

Remoção concluída.

Auditoria (Revisions) ​

Lista o histórico de alterações de um apontamento, incluindo os removidos — somente gestor. É o que sustenta o espelho de ponto numa auditoria ou reclamatória.

http
GET /v1/timesheet/entries/:id/revisions

Exemplo de Resposta ​

json
[
  {
    "id": 7,
    "timesheetId": 4,
    "timesheetEntryId": 6,
    "changedByTimesheetUserId": 5,
    "changedAt": "2026-09-25T12:33:54.859-03:00",
    "operation": "update",
    "reason": "Correção do horário de saída",
    "previousData": {
      "id": 6,
      "kind": "work",
      "endedAt": "2026-09-24T12:00:00.000-03:00",
      "durationMinutes": 180,
      "billingAmount": "480.00",
      "editReason": null
    },
    "currentData": {
      "id": 6,
      "kind": "work",
      "endedAt": "2026-09-24T11:30:00.000-03:00",
      "durationMinutes": 150,
      "billingAmount": 400,
      "editReason": "Correção do horário de saída"
    }
  },
  {
    "id": 4,
    "timesheetId": 4,
    "timesheetEntryId": 6,
    "changedByTimesheetUserId": 5,
    "changedAt": "2026-09-25T12:33:54.359-03:00",
    "operation": "insert",
    "reason": "Lançamento manual de horas esquecidas",
    "previousData": null,
    "currentData": { "id": 6, "kind": "work", "source": "manual" }
  }
]

Cada revisão registra operation (insert, update, delete), reason, o autor (changedByTimesheetUserId) e os snapshots previousData/currentData em JSON — os exemplos acima foram encurtados, mas os snapshots reais contêm todos os atributos do apontamento.