Registro de horas
Los registros de horas (work-logs) representan las horas trabajadas por el personal del club: quién trabajó, cuándo, en qué categoría de tarea, sobre qué cultivo y a qué costo. En esta página profundizaremos en los endpoints de work-logs que permiten consultar, crear, actualizar y eliminar registros de horas de forma programática, además del endpoint de resumen agregado de fuerza laboral. Estas rutas son BFF propias del webapp de Cannahub y se apoyan en Strapi como backend de persistencia.
Todas las rutas requieren autenticación mediante un token JWT en el header Authorization: Bearer {token}. En producción, las operaciones se resuelven contra Strapi y requieren además el header Tenant-id: {tenantId} para aislar los datos por club.
The WorkLog model
El modelo WorkLog describe una jornada o bloque de horas registrado para un trabajador. El campo hours es la unidad de duración expuesta por el BFF; internamente Strapi persiste durationMinutes (horas × 60), y el totalCost se calcula como costPerHour × hours cuando hay un costo por hora definido.
Properties
- Name
id- Type
- string
- Description
Identificador único del registro de horas.
- Name
workerId- Type
- string
- Description
Identificador del trabajador al que pertenece el registro.
- Name
workerName- Type
- string
- Description
Nombre del trabajador (denormalizado para facilitar el listado).
- Name
date- Type
- string
- Description
Fecha del trabajo realizado (formato
YYYY-MM-DD).
- Name
startTime- Type
- string
- Description
Hora de inicio del bloque de trabajo (ISO timestamp). Si no se envía al crear, el servidor genera una hora sintética a partir de la fecha (
08:00).
- Name
endTime- Type
- string
- Description
Hora de fin del bloque de trabajo (ISO timestamp). Si no se envía, se calcula sumando la duración a
startTime.
- Name
hours- Type
- number
- Description
Cantidad de horas trabajadas. Debe ser un número mayor que 0 y menor o igual a 24.
- Name
category- Type
- WorkCategory
- Description
Categoría de la tarea realizada. Puede ser una de:
- INFRASTRUCTURE_MAINTENANCE: Infraestructura/Mantenimiento
- IRRIGATION_FERTILIZATION: Riego/Fertilización
- PROPAGATION_CLONES: Propagación/Clones
- GENERAL_OPERATIONS: Operación general
- LOGISTICS_TRANSPORT: Logística/Traslados
- SANITATION_PEST_CONTROL: Sanidad/Plagas
- PRUNING_DEFOLIATION: Poda/Defoliación
- DAY_LABOR: Jornales/Mano de obra
- Name
description- Type
- string
- Description
Descripción libre de la tarea realizada.
- Name
taskId- Type
- string
- Description
Identificador de la tarea (task) asociada al registro, si proviene de un plan de tareas.
- Name
cropId- Type
- string
- Description
Identificador del cultivo (crop) sobre el que se trabajó.
- Name
cropName- Type
- string
- Description
Nombre del cultivo (denormalizado).
- Name
isJornal- Type
- boolean
- Description
Indica si el registro corresponde a un jornal (mano de obra por día) en lugar de horas comunes. Por defecto
false.
- Name
costPerHour- Type
- number
- Description
Costo por hora del trabajador, en pesos (ARS).
- Name
totalCost- Type
- number
- Description
Costo total del registro, calculado como
costPerHour × hours. Sólo presente cuando existecostPerHour.
- Name
createdAt- Type
- string
- Description
Timestamp de creación del registro (ISO timestamp).
- Name
updatedAt- Type
- string
- Description
Timestamp de la última actualización del registro (ISO timestamp).
Ejemplo de WorkLog
{
"id": "wl_01HQ8XYZWORK01",
"workerId": "wrk_01HQ9TRAB001",
"workerName": "Diego Fernández",
"date": "2025-09-08",
"startTime": "2025-09-08T08:00:00.000Z",
"endTime": "2025-09-08T14:00:00.000Z",
"hours": 6,
"category": "PRUNING_DEFOLIATION",
"description": "Poda apical y defoliación de plantas en floración temprana",
"taskId": "task_01HQ9TASK042",
"cropId": "crop_01HQ9CROP015",
"cropName": "Northern Lights - Sala 2",
"isJornal": false,
"costPerHour": 3500,
"totalCost": 21000,
"createdAt": "2025-09-08T14:05:00.000Z",
"updatedAt": "2025-09-08T14:05:00.000Z"
}
The WorkforceSummary model
El modelo WorkforceSummary expone KPIs agregados de la fuerza laboral para un rango de fechas y, opcionalmente, un trabajador. Es el payload de respuesta del endpoint /api/workforce/summary.
Properties
- Name
dateFrom- Type
- string
- Description
Fecha de inicio del rango consultado (
YYYY-MM-DD) onullsi no se especificó.
- Name
dateTo- Type
- string
- Description
Fecha de fin del rango consultado (
YYYY-MM-DD) onullsi no se especificó.
- Name
workerId- Type
- string
- Description
Identificador del trabajador por el que se filtró, o
nullsi aplica a todo el club.
- Name
tasks- Type
- object
- Description
Métricas agregadas de tareas.
- Name
total- Type
- number
- Description
Total de tareas en el rango.
- Name
completed- Type
- number
- Description
Total de tareas completadas.
- Name
adherenceRate- Type
- number
- Description
Porcentaje de adherencia (tareas completadas sobre el total).
- Name
hours- Type
- object
- Description
Métricas agregadas de horas.
- Name
total- Type
- number
- Description
Total de horas trabajadas en el rango.
- Name
totalMinutes- Type
- number
- Description
Total de minutos trabajados (equivalente en minutos).
- Name
byCategory- Type
- object
- Description
Mapa de horas totales por categoría/tipo de tarea. Las claves corresponden a los tipos de tarea (por ejemplo
WATERING,FEEDING,INSPECTION,TRIMMING,TRAINING,LIGHTING,HARVEST,ENVIRONMENTAL) y los valores son las horas acumuladas.
Ejemplo de WorkforceSummary
{
"dateFrom": "2025-08-01",
"dateTo": "2025-08-31",
"workerId": null,
"tasks": {
"total": 124,
"completed": 98,
"adherenceRate": 79
},
"hours": {
"total": 142.5,
"totalMinutes": 8550
},
"byCategory": {
"WATERING": 32.5,
"FEEDING": 18.0,
"INSPECTION": 24.0,
"TRIMMING": 28.0,
"TRAINING": 14.0,
"LIGHTING": 6.0,
"HARVEST": 12.0,
"ENVIRONMENTAL": 8.0
}
}
Listar registros de horas
Retorna una lista paginada de registros de horas con múltiples filtros opcionales. Por defecto devuelve hasta 50 registros por página.
Query params
- Name
category- Type
- WorkCategory
- Description
Filtra por categoría de trabajo.
- Name
workerId- Type
- string
- Description
Filtra por trabajador.
- Name
cropId- Type
- string
- Description
Filtra por cultivo.
- Name
taskId- Type
- string
- Description
Filtra por tarea.
- Name
dateFrom- Type
- string
- Description
Fecha de inicio del rango (
YYYY-MM-DD).
- Name
dateTo- Type
- string
- Description
Fecha de fin del rango (
YYYY-MM-DD).
- Name
isJornal- Type
- boolean
- Description
Filtra por registros de jornal (
true/false).
- Name
search- Type
- string
- Description
Búsqueda por nombre de trabajador, descripción o nombre de cultivo.
- Name
page- Type
- integer
- Description
Número de página. Por defecto
1.
- Name
limit- Type
- integer
- Description
Cantidad de registros por página. Por defecto
50.
Request
curl -G https://api.cannahub.tech/api/work-logs \
-H "Authorization: Bearer {token}" \
-H "Tenant-id: {tenantId}" \
-d category=PRUNING_DEFOLIATION \
-d dateFrom=2025-09-01 \
-d dateTo=2025-09-30 \
-d limit=50
Response
{
"workLogs": [
{
"id": "wl_01HQ8XYZWORK01",
"workerId": "wrk_01HQ9TRAB001",
"workerName": "Diego Fernández",
"date": "2025-09-08",
"startTime": "2025-09-08T08:00:00.000Z",
"endTime": "2025-09-08T14:00:00.000Z",
"hours": 6,
"category": "PRUNING_DEFOLIATION",
"description": "Poda apical y defoliación de plantas en floración temprana",
"cropId": "crop_01HQ9CROP015",
"cropName": "Northern Lights - Sala 2",
"isJornal": false,
"costPerHour": 3500,
"totalCost": 21000,
"createdAt": "2025-09-08T14:05:00.000Z",
"updatedAt": "2025-09-08T14:05:00.000Z"
}
],
"count": 1,
"filters": {
"category": "PRUNING_DEFOLIATION",
"dateFrom": "2025-09-01",
"dateTo": "2025-09-30",
"page": "1",
"limit": "50"
}
}
Crear un registro de horas
Crea un nuevo registro de horas. Los campos workerId, workerName, date, category y hours son obligatorios. El valor de hours debe ser un número entre 0 (exclusivo) y 24. Si se envía costPerHour, el servidor calcula automáticamente totalCost.
Atributos requeridos
- Name
workerId- Type
- string
- Description
Identificador del trabajador.
- Name
workerName- Type
- string
- Description
Nombre del trabajador.
- Name
date- Type
- string
- Description
Fecha del trabajo (
YYYY-MM-DD).
- Name
category- Type
- WorkCategory
- Description
Categoría de la tarea.
- Name
hours- Type
- number
- Description
Horas trabajadas. Debe ser un número entre 0 y 24 (exclusivo en 0, inclusivo en 24).
Atributos opcionales
- Name
startTime- Type
- string
- Description
Hora de inicio (ISO timestamp). Si se omite, se genera a partir de la fecha (
08:00).
- Name
endTime- Type
- string
- Description
Hora de fin (ISO timestamp). Si se omite, se calcula desde
startTime+ duración.
- Name
description- Type
- string
- Description
Descripción de la tarea.
- Name
taskId- Type
- string
- Description
Identificador de la tarea asociada.
- Name
cropId- Type
- string
- Description
Identificador del cultivo.
- Name
cropName- Type
- string
- Description
Nombre del cultivo.
- Name
isJornal- Type
- boolean
- Description
Marca el registro como jornal. Por defecto
false.
- Name
costPerHour- Type
- number
- Description
Costo por hora del trabajador, en pesos (ARS).
Request
curl -X POST https://api.cannahub.tech/api/work-logs \
-H "Authorization: Bearer {token}" \
-H "Tenant-id: {tenantId}" \
-H "Content-Type: application/json" \
-d '{
"workerId": "wrk_01HQ9TRAB002",
"workerName": "Lucía Romero",
"date": "2025-09-09",
"category": "IRRIGATION_FERTILIZATION",
"hours": 4,
"description": "Riego y fertilización de sala de vegetativo",
"cropId": "crop_01HQ9CROP020",
"cropName": "Amnesia Haze - Sala 1",
"isJornal": false,
"costPerHour": 3200
}'
Response
{
"workLog": {
"id": "wl_01HQ8XYZWORK02",
"workerId": "wrk_01HQ9TRAB002",
"workerName": "Lucía Romero",
"date": "2025-09-09",
"startTime": "2025-09-09T08:00:00.000Z",
"endTime": "2025-09-09T12:00:00.000Z",
"hours": 4,
"category": "IRRIGATION_FERTILIZATION",
"description": "Riego y fertilización de sala de vegetativo",
"cropId": "crop_01HQ9CROP020",
"cropName": "Amnesia Haze - Sala 1",
"isJornal": false,
"costPerHour": 3200,
"totalCost": 12800,
"createdAt": "2025-09-09T12:02:00.000Z",
"updatedAt": "2025-09-09T12:02:00.000Z"
}
}
Obtener un registro de horas
Retorna un único registro de horas por su identificador. Si no existe, devuelve un error 404.
Path params
- Name
id- Type
- string
- Description
Identificador del registro de horas.
Request
curl https://api.cannahub.tech/api/work-logs/wl_01HQ8XYZWORK01 \
-H "Authorization: Bearer {token}" \
-H "Tenant-id: {tenantId}"
Response
{
"workLog": {
"id": "wl_01HQ8XYZWORK01",
"workerId": "wrk_01HQ9TRAB001",
"workerName": "Diego Fernández",
"date": "2025-09-08",
"startTime": "2025-09-08T08:00:00.000Z",
"endTime": "2025-09-08T14:00:00.000Z",
"hours": 6,
"category": "PRUNING_DEFOLIATION",
"description": "Poda apical y defoliación de plantas en floración temprana",
"taskId": "task_01HQ9TASK042",
"cropId": "crop_01HQ9CROP015",
"cropName": "Northern Lights - Sala 2",
"isJornal": false,
"costPerHour": 3500,
"totalCost": 21000,
"createdAt": "2025-09-08T14:05:00.000Z",
"updatedAt": "2025-09-08T14:05:00.000Z"
}
}
Actualizar un registro de horas
Actualiza parcialmente un registro de horas existente. Todos los campos del body son opcionales (partial update). Si se modifican hours o costPerHour, el totalCost se recalcula automáticamente.
Path params
- Name
id- Type
- string
- Description
Identificador del registro de horas.
Atributos opcionales
- Name
workerId- Type
- string
- Description
Identificador del trabajador.
- Name
workerName- Type
- string
- Description
Nombre del trabajador.
- Name
date- Type
- string
- Description
Fecha del trabajo (
YYYY-MM-DD).
- Name
startTime- Type
- string
- Description
Hora de inicio (ISO timestamp).
- Name
endTime- Type
- string
- Description
Hora de fin (ISO timestamp).
- Name
hours- Type
- number
- Description
Horas trabajadas.
- Name
category- Type
- WorkCategory
- Description
Categoría de la tarea.
- Name
description- Type
- string
- Description
Descripción de la tarea.
- Name
taskId- Type
- string
- Description
Identificador de la tarea asociada.
- Name
cropId- Type
- string
- Description
Identificador del cultivo.
- Name
cropName- Type
- string
- Description
Nombre del cultivo.
- Name
isJornal- Type
- boolean
- Description
Marca el registro como jornal.
- Name
costPerHour- Type
- number
- Description
Costo por hora del trabajador, en pesos (ARS).
Request
curl -X PATCH https://api.cannahub.tech/api/work-logs/wl_01HQ8XYZWORK01 \
-H "Authorization: Bearer {token}" \
-H "Tenant-id: {tenantId}" \
-H "Content-Type: application/json" \
-d '{
"hours": 7,
"costPerHour": 3800,
"description": "Poda apical, defoliación y limpieza de sala"
}'
Response
{
"workLog": {
"id": "wl_01HQ8XYZWORK01",
"workerId": "wrk_01HQ9TRAB001",
"workerName": "Diego Fernández",
"date": "2025-09-08",
"hours": 7,
"category": "PRUNING_DEFOLIATION",
"description": "Poda apical, defoliación y limpieza de sala",
"cropId": "crop_01HQ9CROP015",
"cropName": "Northern Lights - Sala 2",
"isJornal": false,
"costPerHour": 3800,
"totalCost": 26600,
"createdAt": "2025-09-08T14:05:00.000Z",
"updatedAt": "2025-09-09T10:20:00.000Z"
}
}
Eliminar un registro de horas
Elimina de forma permanente un registro de horas. Si el registro no existe, devuelve un error 404.
Path params
- Name
id- Type
- string
- Description
Identificador del registro de horas.
Request
curl -X DELETE https://api.cannahub.tech/api/work-logs/wl_01HQ8XYZWORK01 \
-H "Authorization: Bearer {token}" \
-H "Tenant-id: {tenantId}"
Response
{
"success": true
}
Resumen de fuerza laboral
Ruta BFF que retorna KPIs agregados de fuerza laboral para un rango de fechas y, opcionalmente, un trabajador específico. Combina métricas de tareas (total, completadas, adherencia) y de horas (total, minutos, desglose por categoría).
Query params
- Name
dateFrom- Type
- string
- Description
Fecha de inicio del rango (
YYYY-MM-DD).
- Name
dateTo- Type
- string
- Description
Fecha de fin del rango (
YYYY-MM-DD).
- Name
workerId- Type
- string
- Description
Filtra el resumen por un trabajador específico.
Request
curl -G https://api.cannahub.tech/api/workforce/summary \
-H "Authorization: Bearer {token}" \
-H "Tenant-id: {tenantId}" \
-d dateFrom=2025-08-01 \
-d dateTo=2025-08-31
Response
{
"dateFrom": "2025-08-01",
"dateTo": "2025-08-31",
"workerId": null,
"tasks": {
"total": 124,
"completed": 98,
"adherenceRate": 79
},
"hours": {
"total": 142.5,
"totalMinutes": 8550
},
"byCategory": {
"WATERING": 32.5,
"FEEDING": 18.0,
"INSPECTION": 24.0,
"TRIMMING": 28.0,
"TRAINING": 14.0,
"LIGHTING": 6.0,
"HARVEST": 12.0,
"ENVIRONMENTAL": 8.0
}
}
React Query Hooks
Cannahub provee hooks de React Query pre-construidos para gestionar registros de horas. Estos hooks interactúan con las rutas BFF y manejan automáticamente el cacheo, la invalidación y el manejo de errores.
Query Keys
// /features/Club/Workforce/hooks/keys.ts
export const workLogKeys = {
all: ['workLogs'] as const,
lists: () => [...workLogKeys.all, 'list'] as const,
list: (filters?: WorkLogFilters) => [...workLogKeys.lists(), filters] as const,
details: () => [...workLogKeys.all, 'detail'] as const,
detail: (id: string) => [...workLogKeys.details(), id] as const,
}
Queries
import { useWorkLogsQuery, useWorkLogQuery } from '@/features/Club/Workforce/hooks'
// Listar registros de horas con filtros opcionales
const { data, isLoading } = useWorkLogsQuery({
category: 'PRUNING_DEFOLIATION',
dateFrom: '2025-09-01',
dateTo: '2025-09-30',
})
// Obtener un registro de horas por id
const { data: workLog } = useWorkLogQuery('wl_01HQ8XYZWORK01')
Mutations
import {
useCreateWorkLogMutation,
useUpdateWorkLogMutation,
useDeleteWorkLogMutation,
} from '@/features/Club/Workforce/hooks'
// Crear un registro de horas
const { mutate: createWorkLog } = useCreateWorkLogMutation()
createWorkLog({
workerId: 'wrk_01HQ9TRAB002',
workerName: 'Lucía Romero',
date: '2025-09-09',
category: 'IRRIGATION_FERTILIZATION',
hours: 4,
costPerHour: 3200,
})
// Actualizar un registro de horas
const { mutate: updateWorkLog } = useUpdateWorkLogMutation()
updateWorkLog({
id: 'wl_01HQ8XYZWORK01',
data: { hours: 7, costPerHour: 3800 },
})
// Eliminar un registro de horas
const { mutate: deleteWorkLog } = useDeleteWorkLogMutation()
deleteWorkLog('wl_01HQ8XYZWORK01')
Cache Invalidation
Todas las mutaciones invalidan automáticamente las query keys relevantes al completarse con éxito:
// Tras una mutación exitosa, se invalidan estas queries:
queryClient.invalidateQueries({ queryKey: workLogKeys.lists() })
queryClient.invalidateQueries({ queryKey: workLogKeys.detail(id) })
Cálculo de costos y jornales
El costo total de un registro se deriva del costo por hora y las horas trabajadas:
totalCost = costPerHour × hours
Cuando isJornal es true, el registro representa un jornal (mano de obra por día) en lugar de horas comunes; el cálculo de costo sigue la misma fórmula sobre las horas registradas. Internamente, el BFF convierte hours a durationMinutes (hours × 60) al persistir en Strapi, y recompone hours al leer.