Feat: Implementar página de Informe de Empleados completa
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m0s

- Crear 5 queries SQL en Metabase para datos de empleados:
  * Contadores generales (horas, días, tareas)
  * Lista de empleados con totales agregados
  * Detalle de tareas realizadas
  * Detalle de asistencias con cálculo de horas
  * Opciones de filtros disponibles

- Implementar backend API endpoint /api/metabase/informe-empleados
  * Soporte para filtros por fecha, empleados, títulos de tareas y planillas
  * Ejecución paralela de queries con manejo de errores
  * Transformación de datos de Metabase a formato consumible

- Crear componente TotalesEmpleados.vue
  * Visualización de métricas principales (horas, días, tareas)
  * Cálculo de promedios por empleado
  * Funcionalidad de copiar texto/JSON

- Implementar página informe-empleados.vue
  * Layout tipo informe con selector de fechas
  * Filtros avanzados por empleado, títulos de tareas y planillas
  * Tabla integrada de empleados con métricas clave
  * Estados de carga, error y bienvenida
  * Detección de cambios pendientes

- Actualizar configuración de queries en metabase-queries.ts

Estructura trabajada:
- clientes (empleado = true)
- asistencias (con cálculo de horas trabajadas)
- tareas_realizadas (con títulos y planillas)
- planillas (con totales y rangos de fechas)
This commit is contained in:
2025-11-13 14:10:05 -06:00
parent 9c6c423ca9
commit 98c2f2edac
4 changed files with 845 additions and 0 deletions

View File

@@ -0,0 +1,183 @@
import { METABASE_QUERIES } from '../../config/metabase-queries'
/**
* Execute all informe empleados queries in parallel
* Returns data for the Informe de Empleados page
*/
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const {
fecha_desde = null,
fecha_hasta = null,
empleado_ids = [],
titulos_tareas = [],
titulos_planillas = []
} = body
try {
// First, get all cards to find our informe empleados queries
const allCards = await getMetabaseCards('all')
// Find our informe empleados queries by name using centralized config
const queryNames = METABASE_QUERIES.informe_empleados
const cards: Record<string, any> = {}
for (const [key, name] of Object.entries(queryNames)) {
const card = allCards.find((c: any) => c.name === name)
if (!card) {
console.warn(`[Informe Empleados] Query not found: ${name}`)
} else {
cards[key] = card
}
}
// Build parameters array for Metabase queries
const buildParameters = () => {
const params = [
{
type: 'text',
target: ['variable', ['template-tag', 'fecha_desde']],
value: fecha_desde || ''
},
{
type: 'text',
target: ['variable', ['template-tag', 'fecha_hasta']],
value: fecha_hasta || ''
}
]
// Solo agregar filtros opcionales si tienen valores (no vacíos)
if (empleado_ids && Array.isArray(empleado_ids) && empleado_ids.length > 0) {
params.push({
type: 'number',
target: ['variable', ['template-tag', 'empleado_ids']],
value: empleado_ids
})
}
if (titulos_tareas && Array.isArray(titulos_tareas) && titulos_tareas.length > 0) {
params.push({
type: 'text',
target: ['variable', ['template-tag', 'titulos_tareas']],
value: titulos_tareas
})
}
if (titulos_planillas && Array.isArray(titulos_planillas) && titulos_planillas.length > 0) {
params.push({
type: 'text',
target: ['variable', ['template-tag', 'titulos_planillas']],
value: titulos_planillas
})
}
return params
}
const standardParams = buildParameters()
const emptyParams: any[] = [] // Para opciones_filtros que no requiere parámetros
// Execute all queries in parallel with error handling
const executeWithErrorHandling = async (name: string, cardId: number | undefined, parameters: any[], defaultValue: any) => {
if (!cardId) {
console.warn(`[Informe Empleados] No card ID for ${name}`)
return defaultValue
}
try {
console.log(`[Informe Empleados] Executing query: ${name} (ID: ${cardId})`)
const result = await executeCardQuery(cardId, parameters)
console.log(`[Informe Empleados] Query ${name} returned ${result.data?.rows?.length || 0} rows`)
return result
} catch (error: any) {
console.error(`[Informe Empleados] Error executing ${name}:`, error.message)
return defaultValue
}
}
const [
contadores,
listaEmpleados,
detalleTareas,
detalleAsistencias,
opcionesFiltros
] = await Promise.all([
executeWithErrorHandling('contadores', cards.contadores?.id, standardParams, { data: { rows: [[]], cols: [] } }),
executeWithErrorHandling('lista_empleados', cards.lista_empleados?.id, standardParams, { data: { rows: [], cols: [] } }),
executeWithErrorHandling('detalle_tareas', cards.detalle_tareas?.id, standardParams, { data: { rows: [], cols: [] } }),
executeWithErrorHandling('detalle_asistencias', cards.detalle_asistencias?.id, standardParams, { data: { rows: [], cols: [] } }),
executeWithErrorHandling('opciones_filtros', cards.opciones_filtros?.id, emptyParams, { data: { rows: [], cols: [] } })
])
// Transform Metabase responses to objects for easier frontend consumption
const transformSingleRow = (result: any) => {
if (!result.data?.rows?.[0] || !result.data?.cols) return {}
const row = result.data.rows[0]
const cols = result.data.cols
const obj: any = {}
cols.forEach((col: any, index: number) => {
obj[col.name] = row[index]
})
return obj
}
const transformMultipleRows = (result: any) => {
if (!result.data?.rows || !result.data?.cols) return []
const cols = result.data.cols
return result.data.rows.map((row: any[]) => {
const obj: any = {}
cols.forEach((col: any, index: number) => {
obj[col.name] = row[index]
})
return obj
})
}
// Transform opciones_filtros to a more usable format
const transformOpcionesFiltros = (result: any) => {
if (!result.data?.rows || !result.data?.cols) {
return {
titulos_tareas: [],
titulos_planillas: []
}
}
const rows = transformMultipleRows(result)
const opciones: any = {
titulos_tareas: [],
titulos_planillas: []
}
rows.forEach((row: any) => {
if (row.tipo_opcion === 'titulos_tareas') {
opciones.titulos_tareas.push(row.valor)
} else if (row.tipo_opcion === 'titulos_planillas') {
opciones.titulos_planillas.push(row.valor)
}
})
return opciones
}
// Return all data in a structured format
return {
contadores: transformSingleRow(contadores),
listaEmpleados: transformMultipleRows(listaEmpleados),
detalleTareas: transformMultipleRows(detalleTareas),
detalleAsistencias: transformMultipleRows(detalleAsistencias),
opcionesFiltros: transformOpcionesFiltros(opcionesFiltros)
}
} catch (error: any) {
console.error('[API] Failed to execute informe empleados queries:', error)
throw createError({
statusCode: error.statusCode || 500,
statusMessage: error.statusMessage || 'Failed to execute informe empleados queries'
})
}
})

View File

@@ -57,6 +57,17 @@ export const METABASE_QUERIES = {
opciones_filtros: 'Informe Comercios - Opciones de Filtros',
contadores: 'Informe Comercios - Contadores de Filtros',
detalle_ingresos: 'Informe Comercios - Detalle de Ingresos por Comercio'
},
/**
* Queries para Informe de Empleados
*/
informe_empleados: {
contadores: 'Informe Empleados - Contadores',
lista_empleados: 'Informe Empleados - Lista con Totales',
detalle_tareas: 'Informe Empleados - Detalle Tareas',
detalle_asistencias: 'Informe Empleados - Detalle Asistencias',
opciones_filtros: 'Informe Empleados - Opciones Filtros'
}
} as const
@@ -68,3 +79,4 @@ export type PanoramaQueryKey = keyof typeof METABASE_QUERIES.panorama
export type InformeQueryKey = keyof typeof METABASE_QUERIES.informe
export type ComparativaQueryKey = keyof typeof METABASE_QUERIES.comparativa
export type InformeComerciosQueryKey = keyof typeof METABASE_QUERIES.informe_comercios
export type InformeEmpleadosQueryKey = keyof typeof METABASE_QUERIES.informe_empleados