Files
analiticaNucleo/nuxt4-app/app/components/empleados/DetalleAsistencias.vue
josedario87 5fe4a3af5c
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 52s
Feat: Agregar altura máxima a tablas de tareas y asistencias en informe empleados
- Agregado max-h-96 y overflow-y-auto a DetalleTareas.vue
- Agregado max-h-96 y overflow-y-auto a DetalleAsistencias.vue
- Las tablas ahora tienen scroll vertical cuando hay muchos registros
2025-11-13 18:32:37 -06:00

229 lines
8.0 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<UCard class="brand-card border border-transparent">
<template #header>
<div class="flex items-center justify-between">
<h2 class="text-xl font-bold brand-section-title">Detalle de Asistencias</h2>
<div class="flex items-center gap-2">
<UBadge color="gray" variant="soft" size="sm">
{{ data.length }} registros
</UBadge>
<UButton
size="xs"
color="gray"
variant="soft"
icon="i-lucide-copy"
@click="copiarTexto"
>
Copiar Texto
</UButton>
<UButton
size="xs"
color="gray"
variant="soft"
icon="i-lucide-braces"
@click="copiarJSON"
>
Copiar JSON
</UButton>
</div>
</div>
</template>
<div class="overflow-x-auto max-h-96 overflow-y-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-[var(--brand-border)]">
<th class="text-left py-3 px-3 font-semibold text-[var(--brand-text)]">Empleado</th>
<th class="text-center py-3 px-3 font-semibold text-[var(--brand-text)]">Fecha</th>
<th class="text-center py-3 px-3 font-semibold text-[var(--brand-text)]">Entrada</th>
<th class="text-center py-3 px-3 font-semibold text-[var(--brand-text)]">Salida</th>
<th class="text-right py-3 px-3 font-semibold text-[var(--brand-text)]">Horas</th>
<th class="text-center py-3 px-3 font-semibold text-[var(--brand-text)]">Estado</th>
<th class="text-left py-3 px-3 font-semibold text-[var(--brand-text)]">Observación</th>
</tr>
</thead>
<tbody>
<tr
v-for="asistencia in data"
:key="asistencia.asistencia_id"
class="border-b border-[var(--brand-border)] hover:bg-[var(--brand-surface)] transition-colors"
>
<td class="py-3 px-3">
<div class="text-[var(--brand-text)] font-medium">{{ asistencia.empleado_nombre }}</div>
<div class="text-[var(--brand-text-muted)] text-xs font-mono">{{ asistencia.empleado_cedula || 'N/A' }}</div>
</td>
<td class="py-3 px-3 text-center text-[var(--brand-text)]">
{{ formatDate(asistencia.fecha_asistencia) }}
</td>
<td class="py-3 px-3 text-center">
<span v-if="asistencia.entrada" class="font-mono text-green-400">
{{ formatTime(asistencia.entrada) }}
</span>
<span v-else class="text-[var(--brand-text-muted)] text-xs italic">
Sin registro
</span>
</td>
<td class="py-3 px-3 text-center">
<span v-if="asistencia.salida" class="font-mono text-orange-400">
{{ formatTime(asistencia.salida) }}
</span>
<span v-else class="text-[var(--brand-text-muted)] text-xs italic">
Sin registro
</span>
</td>
<td class="py-3 px-3 text-right">
<span
v-if="asistencia.horas_trabajadas !== null"
class="font-mono font-bold text-blue-400"
>
{{ formatHours(asistencia.horas_trabajadas) }}
</span>
<span v-else class="text-[var(--brand-text-muted)] text-xs">
-
</span>
</td>
<td class="py-3 px-3 text-center">
<UBadge
:color="getEstadoColor(asistencia.estado)"
variant="soft"
size="xs"
>
{{ asistencia.estado || 'registrada' }}
</UBadge>
</td>
<td class="py-3 px-3 text-[var(--brand-text-muted)] text-xs max-w-xs truncate">
{{ asistencia.observacion || '-' }}
</td>
</tr>
</tbody>
<tfoot v-if="data.length > 0" class="border-t-2 border-[var(--brand-border)]">
<tr class="bg-[var(--brand-surface)]">
<td colspan="4" class="py-3 px-3 font-semibold text-[var(--brand-text)]">
TOTALES ({{ data.length }} registros)
</td>
<td class="py-3 px-3 text-right">
<span class="font-mono font-bold text-blue-400">
{{ formatHours(totalHoras) }}
</span>
<div class="text-xs text-[var(--brand-text-muted)] mt-1">
{{ formatDays(totalHoras) }} días equiv.
</div>
</td>
<td colspan="2"></td>
</tr>
</tfoot>
</table>
<div v-if="data.length === 0" class="text-center py-12 text-[var(--brand-text-muted)]">
<div class="i-lucide-inbox text-4xl mx-auto mb-3 opacity-50"></div>
<p>No hay asistencias en el rango de fechas seleccionado</p>
</div>
</div>
</UCard>
</template>
<script setup lang="ts">
interface Asistencia {
asistencia_id: number
empleado_id: number
empleado_nombre: string
empleado_cedula: string
entrada: string | null
salida: string | null
horas_trabajadas: number | null
estado: string
observacion: string | null
fecha_asistencia: string
}
const props = defineProps<{
data: Asistencia[]
rangoLegible: string
lastUpdated: string
}>()
// Computed para totales
const totalHoras = computed(() => {
return props.data.reduce((sum, a) => sum + (a.horas_trabajadas || 0), 0)
})
// Funciones helper
const getEstadoColor = (estado: string) => {
const colores: Record<string, string> = {
'registrada': 'green',
'pendiente': 'yellow',
'anulado': 'red',
'justificada': 'blue',
'tardanza': 'orange'
}
return colores[estado?.toLowerCase()] || 'green'
}
const formatDate = (dateString: string) => {
if (!dateString) return 'N/A'
const date = new Date(dateString)
return new Intl.DateTimeFormat('es-HN', {
weekday: 'short',
year: 'numeric',
month: 'short',
day: 'numeric'
}).format(date)
}
const formatTime = (dateTimeString: string) => {
if (!dateTimeString) return 'N/A'
const date = new Date(dateTimeString)
return new Intl.DateTimeFormat('es-HN', {
hour: '2-digit',
minute: '2-digit',
hour12: true
}).format(date)
}
const formatHours = (hours: number) => {
if (hours === null || hours === undefined) return '0.00 hrs'
return `${hours.toFixed(2)} hrs`
}
const formatDays = (hours: number) => {
const days = hours / 8
return days.toFixed(2)
}
async function copiarTexto() {
const footer = `
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 RESUMEN
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📅 Rango: ${props.rangoLegible}
📋 Asistencias: ${props.data.length}
🕐 Generado: ${props.lastUpdated}`
let texto = `⏰ DETALLE DE ASISTENCIAS\n\n`
props.data.forEach((asistencia, idx) => {
texto += `${idx + 1}. ${asistencia.empleado_nombre} (${asistencia.empleado_cedula || 'N/A'})\n`
texto += ` Fecha: ${formatDate(asistencia.fecha_asistencia)}\n`
texto += ` Entrada: ${asistencia.entrada ? formatTime(asistencia.entrada) : 'Sin registro'}\n`
texto += ` Salida: ${asistencia.salida ? formatTime(asistencia.salida) : 'Sin registro'}\n`
texto += ` Horas: ${formatHours(asistencia.horas_trabajadas || 0)}\n`
texto += ` Estado: ${asistencia.estado || 'registrada'}\n`
if (asistencia.observacion) {
texto += ` Observación: ${asistencia.observacion}\n`
}
texto += `\n`
})
texto += `\n⏱ TOTAL HORAS: ${formatHours(totalHoras.value)} (${formatDays(totalHoras.value)} días equiv.)${footer}`
await navigator.clipboard.writeText(texto)
alert('✅ Detalle de Asistencias copiado al portapapeles')
}
async function copiarJSON() {
const json = JSON.stringify(props.data, null, 2)
await navigator.clipboard.writeText(json)
alert('✅ JSON copiado al portapapeles')
}
</script>