All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 52s
- 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
229 lines
8.0 KiB
Vue
229 lines
8.0 KiB
Vue
<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>
|