Appearance
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
| Campo | Tipo | Descrição |
|---|---|---|
name | String | Nome do local (máx. 150 caracteres). |
address | String | Endereço (máx. 250 caracteres, opcional). |
latitude / longitude | Number | Coordenadas do centro da cerca. |
radiusMeters | Number | Raio em metros (inteiro, mín. 1). |
isActive | Boolean | Somente 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/:idListagens 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)
| Campo | Tipo | Descrição |
|---|---|---|
kind | String | in ou out. |
source | String | Origem: web (padrão), mobile, kiosk, api, import ou manual. |
punchedAt | DateTime | Instante da batida. |
latitude / longitude / accuracyMeters | Number | Coordenadas e precisão (opcionais). |
address | String | Endereço resolvido (opcional). |
timesheetLocationId | Number | Local mais próximo (preenchido pela aplicação). |
isWithinGeofence / distanceMeters | Boolean / Number | Resultado da validação da geofence (preenchido pela aplicação). |
deviceId / deviceModel | String | Identificação do aparelho (opcional). |
ipAddress | String | IP de origem (capturado pela aplicação). |
photoUrl | String | Foto 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/inPayload (Body)
| Atributo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
punchedAt | datetime | Não | Instante da batida (ISO 8601 ou YYYY-MM-DD HH:mm:ss; padrão: agora). |
kind | string | Não | Tipo do apontamento: work (padrão), break, travel, on_call ou training. |
source | string | Não | Origem (padrão: web). |
latitude / longitude | number | Não | Coordenadas da batida. |
accuracyMeters | number | Não | Precisão em metros. |
address | string | Não | Endereço (máx. 250 caracteres). |
deviceId / deviceModel | string | Não | Identificação do aparelho (máx. 150 caracteres). |
photoUrl | string | Não | URL 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/outMesmo 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/currentValidaçã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ção | Erro |
|---|---|
| Batida sem coordenadas | 422 GEOLOCATION_REQUIRED |
| Nenhum local ativo cadastrado | 422 OUTSIDE_GEOFENCE |
| Fora do raio de todos os locais | 422 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
| Code | Status | Significado |
|---|---|---|
OPEN_ENTRY_EXISTS | 422 | Já existe um apontamento em aberto. |
NO_OPEN_ENTRY | 422 | Saída sem apontamento aberto. |
MAX_INTERVALS_REACHED | 422 | Limite diário de intervalos atingido. |
INVALID_PUNCH_INTERVAL | 422 | Saída anterior à entrada ou apontamento com mais de 24h. |
GEOLOCATION_REQUIRED | 422 | Coordenadas obrigatórias ausentes. |
OUTSIDE_GEOFENCE | 422 | Fora da área permitida (ou sem locais ativos). |
DAY_LOCKED | 422 | O dia está approved ou locked. |
PERIOD_CLOSED | 422 | A 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
| Campo | Tipo | Descrição |
|---|---|---|
workDate | Date | Data local do dia (YYYY-MM-DD). |
timezone | String | Fuso usado para resolver a data. |
status | String | open, submitted, approved, rejected ou locked. |
expectedMinutes | Number | Minutos esperados (jornada; 0 em feriado/ausência). |
workedMinutes | Number | Minutos trabalhados (soma dos apontamentos fechados, exceto break). |
breakMinutes | Number | Minutos de intervalo (apontamentos break). |
balanceMinutes | Number | Saldo: workedMinutes - expectedMinutes. |
isHoliday | Boolean | O dia é feriado não útil. |
notes | String | Observações (opcional). |
submittedAt / approvedAt / lockedAt | DateTime | Marcos do fluxo de aprovação. |
approvedByTimesheetUserId | Number | Gestor 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 paraopenerejected.approve/reject/lock: somente gestores.rejectdevolve 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/lockGET /me/days: os dias do próprio profissional (filtrosstatus,startsOn/endsOn).GET /days: todos os dias da instância (somente gestor), com filtro adicionaltimesheetUserId.GET /days/:id: retorna o dia com os apontamentos (timesheetEntries) ordenados porsequence. Profissionais só enxergam os próprios dias (404para 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
| Campo | Tipo | Descrição |
|---|---|---|
timesheetDayId | Number | Dia consolidado ao qual o apontamento pertence. |
timesheetUserId | Number | Profissional dono do apontamento. |
timesheetProjectId / timesheetTaskId | Number | Projeto/tarefa vinculados (opcionais) — ver Clientes, Projetos & Tarefas. |
sequence | Number | Ordem do apontamento no dia. |
kind | String | work, break, travel, on_call ou training. |
source | String | web, mobile, kiosk, api, import ou manual. |
startedAt / endedAt | DateTime | Início e fim (endedAt: null = apontamento aberto). |
durationMinutes | Number | Duração calculada (minutos). |
description | String | Descrição livre (opcional). |
isBillable | Boolean | Indica se a hora é faturável. |
costRate / billingRate / rateMultiplier | Number | Snapshot financeiro congelado — ver abaixo. |
currency | String | Moeda do snapshot. |
costAmount / billingAmount | Number | Custo e faturamento calculados. |
editReason | String | Motivo da última correção manual. |
Snapshot Financeiro
Ao fechar um apontamento, a aplicação congela:
- Tarifa vigente do profissional na data do dia (
hourlyCost,hourlyBillingRate). - Multiplicador: feriado (
holidayMultiplier) tem prioridade; sábado/domingo usamweekendMultiplier; demais dias,1. - Valor-hora de venda com precedência: alocação do projeto (
project_users.hourlyBillingRate) → valor-hora do projeto → tarifa do profissional. - Supressão de faturamento:
isBillable: falseou projetonon_billable/fixed_pricezera 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/:idGET /entries: todos os apontamentos (somente gestor), com filtrostimesheetUserId,timesheetProjectId,kindestartsOn/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 (404para 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/entriesPayload (Body)
| Atributo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
timesheetUserId | number | Não | Profissional (padrão: o autenticado; para terceiros, exige gestor — 403 FORBIDDEN). |
timesheetProjectId | number | Não | Projeto existente na instância. |
timesheetTaskId | number | Não | Tarefa existente na instância. |
kind | string | Sim | work, break, travel, on_call ou training. |
startedAt | datetime | Sim | Início (ISO 8601). |
endedAt | datetime | Não | Fim (posterior a startedAt, máx. 24h; null = aberto). |
description | string | Não | Descrição livre. |
editReason | string | Sim | Motivo do lançamento (mín. 3 caracteres) — gravado na revisão. |
Validações de Negócio
| Situação | Erro |
|---|---|
| Instância não permite apontamento manual | 422 MANUAL_ENTRY_NOT_ALLOWED |
| Já existe apontamento aberto para o profissional | 422 OPEN_ENTRY_EXISTS |
| Sobreposição com outro apontamento | 422 ENTRY_OVERLAP |
| Fim anterior ao início ou duração > 24h | 422 INVALID_DATE_RANGE |
| Data em período fechado | 422 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/:idMesmos 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/:idRespostas
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/revisionsExemplo 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.