Merge remote-tracking branch 'github/main'
Some checks failed
build-and-deploy / filter (push) Successful in 2s
Sync to GitHub / sync (push) Failing after 1s
build-and-deploy / build (push) Successful in 11s
build-and-deploy / deploy (push) Successful in 15s

This commit is contained in:
2025-06-03 14:03:39 -06:00
11 changed files with 638 additions and 524 deletions

View File

@@ -1,41 +1,30 @@
<template>
<div :style="{ backgroundColor: ui.tableBgColorAsistencias }" class="shadow-md rounded-lg p-4 md:p-6 m-2 border border-gray-200 hover:shadow-lg transition-shadow duration-300 ease-in-out flex flex-col">
<div class="flex justify-between items-center mb-3 md:mb-4 pb-2 md:pb-3 border-b border-gray-100">
<h4 class="text-lg md:text-xl font-semibold" :style="{ color: 'var(--accent-color-asistencias)' }">Asistencia ID: {{ asistencia.id }}</h4>
<span :class="['px-2 py-1 text-xs font-bold text-white rounded-full', getStatusClass(asistencia.estado)]">
{{ asistencia.estado || 'N/A' }}
</span>
</div>
<div class="text-sm text-gray-700 space-y-2">
<p><strong class="font-medium text-gray-900">Empleado ID:</strong> {{ asistencia.empleado_id }}</p>
<p><strong class="font-medium text-gray-900">Entrada:</strong> {{ formatDateTime(asistencia.entrada) }}</p>
<p><strong class="font-medium text-gray-900">Salida:</strong> {{ asistencia.salida ? formatDateTime(asistencia.salida) : 'No registrada' }}</p>
<p v-if="asistencia.observacion" class="italic text-gray-600 bg-gray-50 p-2 border-l-3 rounded" :style="{ borderColor: 'var(--accent-color-asistencias)' }">
<strong class="font-medium text-gray-900">Observación:</strong> {{ asistencia.observacion }}
</p>
</div>
<div class="mt-auto pt-3 md:pt-4 flex justify-end space-x-2 md:space-x-3">
<button
@click="editAsistencia"
class="px-3 py-1 md:px-4 md:py-2 text-xs md:text-sm font-medium rounded-md transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2 text-white"
:style="{ backgroundColor: 'var(--accent-color-asistencias)', borderColor: 'var(--accent-color-asistencias)' }"
@mouseover="buttonHover($event, true, 'var(--accent-color-asistencias)')"
@mouseleave="buttonHover($event, false, 'var(--accent-color-asistencias)')"
:class="`focus:ring-[var(--accent-color-asistencias)]`"
>Editar</button>
<button
@click="confirmDeleteAsistencia"
class="px-3 py-1 md:px-4 md:py-2 text-xs md:text-sm font-medium rounded-md transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2 bg-red-600 hover:bg-red-700 text-white focus:ring-red-500"
>Eliminar</button>
</div>
</div>
<NucleoDataCard
:title="`Asistencia ID: ${asistencia.id}`"
:status="asistencia.estado"
:fields="cardFields"
:accent-color="ui.accentColorAsistencias"
:background-color="ui.tableBgColorAsistencias"
:observation="asistencia.observacion"
@edit="editAsistencia"
@delete="confirmDeleteAsistencia"
>
<template #header>
<div class="flex items-center justify-between w-full">
<h2 class="text-xl font-semibold text-gray-800" :style="{ color: ui.accentColorAsistencias }">Asistencia ID: {{ asistencia.id }}</h2>
<span :class="['px-2 py-1 text-xs font-bold text-white rounded-full', getStatusClass(asistencia.estado)]">
{{ asistencia.estado || 'N/A' }}
</span>
</div>
</template>
</NucleoDataCard>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue';
import { defineProps, defineEmits, computed } from 'vue';
import { useAsistenciasStore } from '../../stores/useAsistencias';
import { useUi } from '../../stores/useUi.js';
import { computed } from 'vue';
import NucleoDataCard from '../ui/NucleoDataCard.vue';
const ui = useUi();
@@ -53,15 +42,23 @@ const asistenciasStore = useAsistenciasStore();
const formatDateTime = (dateTimeString) => {
if (!dateTimeString) return 'N/A';
const date = new Date(dateTimeString);
return date.toLocaleString('es-HN', { timeZone: 'America/Tegucigalpa', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
// Consistent formatting
return date.toLocaleString('es-HN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: true });
};
const cardFields = computed(() => [
{ label: 'Empleado ID', value: props.asistencia.empleado_id },
{ label: 'Entrada', value: formatDateTime(props.asistencia.entrada) },
{ label: 'Salida', value: props.asistencia.salida ? formatDateTime(props.asistencia.salida) : 'No registrada' },
]);
const editAsistencia = () => {
emit('edit', props.asistencia.id);
};
const confirmDeleteAsistencia = () => {
if (confirm(`¿Está seguro de que desea eliminar la asistencia "${props.asistencia.id}" (ID: ${props.asistencia.id})?`)) {
// Consider using a global notification/dialog system if available
if (confirm(`¿Está seguro de que desea eliminar la asistencia ID: ${props.asistencia.id}?`)) {
deleteAsistenciaInternal();
}
};
@@ -69,48 +66,34 @@ const confirmDeleteAsistencia = () => {
const deleteAsistenciaInternal = async () => {
try {
await asistenciasStore.deleteAsistencia(props.asistencia.id);
// Optionally, show a success notification
} catch (error) {
console.error('Error deleting asistencia:', error);
// Optionally, show an error notification
alert('Ocurrió un error al eliminar la asistencia.');
}
};
const getStatusClass = (status) => {
if (!status) return 'bg-gray-400'; // Default for N/A
if (!status) return 'bg-gray-400 text-white'; // Default for N/A, ensuring text is visible
const statusNormalized = status.toLowerCase().replace(/\s+/g, '-');
switch (statusNormalized) {
case 'pendiente': return 'bg-yellow-500 text-gray-800';
case 'presente':
case 'confirmada': return 'bg-green-500';
case 'ausente': return 'bg-red-500';
case 'justificada': return 'bg-blue-500';
case 'confirmada': return 'bg-green-500 text-white';
case 'ausente': return 'bg-red-500 text-white';
case 'justificada': return 'bg-blue-500 text-white';
case 'cancelada':
case 'anulada': return 'bg-gray-500';
default: return 'bg-gray-400';
}
};
const buttonHover = (event, isHovering, colorVar) => {
if (isHovering) {
event.target.style.filter = 'brightness(90%)';
} else {
event.target.style.filter = 'brightness(100%)';
case 'anulada': return 'bg-gray-500 text-white';
default: return 'bg-gray-400 text-white';
}
};
</script>
<style scoped>
/* Minimal scoped styles, mainly for complex status colors if not handled by Tailwind directly */
/* Or for :style bindings if preferred for dynamic properties like accent colors */
/* The border-l-3 for observation uses a utility-like class,
but Tailwind doesn't have border-left-width: 3px by default.
You could add this to your tailwind.config.js if used often:
theme: { extend: { borderWidth: { '3': '3px' } } }
Then use class `border-l-3`. For now, inline style is fine.
/* Scoped styles can be kept if they target elements within slots that NucleoDataCard doesn't style.
For this refactor, assuming NucleoDataCard and Tailwind handle most styling.
The .border-l-3 class is no longer used as observation styling is handled by NucleoDataCard.
*/
.border-l-3 {
border-left-width: 3px;
}
</style>

View File

@@ -1,66 +1,43 @@
<template>
<div
class="p-4 sm:p-6 rounded-lg overflow-x-auto"
:style="{ backgroundColor: ui.tableBgColorAsistencias }"
<NucleoTable
:columns="columns"
:items="props.asistencias"
accent-color="--accent-color-asistencias"
table-bg-color-name="tableBgColorAsistencias"
>
<table
class="min-w-full divide-y divide-[var(--accent-color-asistencias)]"
:style="{ backgroundColor: ui.tableBgColorAsistencias }"
>
<thead
class="divide-y divide-[var(--accent-color-asistencias)]"
:style="{ backgroundColor: ui.tableBgColorAsistencias }"
>
<tr>
<th class="px-4 py-3 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 dark:text-slate-400 uppercase tracking-wider">ID</th>
<th class="px-4 py-3 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 dark:text-slate-400 uppercase tracking-wider">Empleado ID</th>
<th class="px-4 py-3 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 dark:text-slate-400 uppercase tracking-wider">Entrada</th>
<th class="px-4 py-3 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 dark:text-slate-400 uppercase tracking-wider">Salida</th>
<th class="px-4 py-3 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 dark:text-slate-400 uppercase tracking-wider">Estado</th>
<th class="px-4 py-3 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 dark:text-slate-400 uppercase tracking-wider">Observación</th>
<th class="px-4 py-3 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 dark:text-slate-400 uppercase tracking-wider">Acciones</th>
</tr>
</thead>
<tbody
class="divide-y divide-[var(--accent-color-asistencias)]"
:style="{ backgroundColor: ui.tableBgColorAsistencias }"
>
<tr v-if="!asistencias || asistencias.length === 0">
<td colspan="7" class="px-6 py-10 text-center text-gray-500 dark:text-slate-400 text-lg">No hay asistencias para mostrar.</td>
</tr>
<tr v-for="asistencia in asistencias" :key="asistencia.id" class="transition-colors duration-150 ease-in-out hover:bg-[var(--accent-color-asistencias)]/10 dark:hover:bg-[var(--accent-color-asistencias)]/20">
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700 dark:text-slate-300">{{ asistencia.id }}</td>
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700 dark:text-slate-300">{{ asistencia.empleado_id }}</td>
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700 dark:text-slate-300">{{ formatDateTime(asistencia.entrada) }}</td>
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700 dark:text-slate-300">{{ asistencia.salida ? formatDateTime(asistencia.salida) : 'N/A' }}</td>
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700 dark:text-slate-300">
<span :class="['px-2.5 py-0.5 rounded-full text-xs font-semibold', getStatusClass(asistencia.estado)]">{{ asistencia.estado }}</span>
</td>
<td class="px-4 py-3 sm:px-6 sm:py-4 text-sm text-gray-700 dark:text-slate-300 max-w-xs whitespace-nowrap" :title="asistencia.observacion">{{ truncateText(asistencia.observacion, 50) }}</td>
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700 dark:text-slate-300">
<div class="flex items-center space-x-2">
<button @click="editAsistencia(asistencia.id)" class="p-1.5 sm:p-2 rounded-md transition-all duration-150 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2 text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 hover:bg-blue-100 dark:hover:bg-blue-600/20 focus:ring-blue-500 dark:focus:ring-blue-400" title="Editar">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10" /></svg>
</button>
<button @click="confirmDeleteAsistencia(asistencia)" class="p-1.5 sm:p-2 rounded-md transition-all duration-150 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2 text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300 hover:bg-red-100 dark:hover:bg-red-600/20 focus:ring-red-500 dark:focus:ring-red-400" title="Eliminar">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12.56 0c1.153 0 2.24.03 3.22.077m3.22-.077L10.879 3.28a2.25 2.25 0 012.244-2.077h.093c.956 0 1.853.543 2.244 2.077L14.74 5.79m-4.858 0l-2.828-2.828A1.875 1.875 0 016.188 2.188l2.828 2.828m6.912 0l2.828-2.828a1.875 1.875 0 00-2.652-2.652L12 5.79M9.26 9h5.48L9.26 9z" /></svg>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<template #cell-entrada="{ item }">
{{ formatDateTime(item.entrada) }}
</template>
<template #cell-salida="{ item }">
{{ item.salida ? formatDateTime(item.salida) : 'N/A' }}
</template>
<template #cell-estado="{ item }">
<span :class="['px-2.5 py-0.5 rounded-full text-xs font-semibold', getStatusClass(item.estado)]">
{{ item.estado }}
</span>
</template>
<template #cell-observacion="{ item }">
<span :title="item.observacion">{{ truncateText(item.observacion, 50) }}</span>
</template>
<template #actions="{ item }">
<div class="flex items-center space-x-2">
<button @click="editAsistencia(item.id)" class="p-1.5 sm:p-2 rounded-md transition-all duration-150 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2 text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 hover:bg-blue-100 dark:hover:bg-blue-600/20 focus:ring-blue-500 dark:focus:ring-blue-400" title="Editar">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10" /></svg>
</button>
<button @click="confirmDeleteAsistencia(item)" class="p-1.5 sm:p-2 rounded-md transition-all duration-150 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2 text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300 hover:bg-red-100 dark:hover:bg-red-600/20 focus:ring-red-500 dark:focus:ring-red-400" title="Eliminar">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12.56 0c1.153 0 2.24.03 3.22.077m3.22-.077L10.879 3.28a2.25 2.25 0 012.244-2.077h.093c.956 0 1.853.543 2.244 2.077L14.74 5.79m-4.858 0l-2.828-2.828A1.875 1.875 0 016.188 2.188l2.828 2.828m6.912 0l2.828-2.828a1.875 1.875 0 00-2.652-2.652L12 5.79M9.26 9h5.48L9.26 9z" /></svg>
</button>
</div>
</template>
</NucleoTable>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue';
import NucleoTable from '../ui/NucleoTable.vue'; // Import NucleoTable
import { useAsistenciasStore } from '../../stores/useAsistencias';
import { formatDateTime, truncateText, getStatusClass } from '../../utils/formatters.js';
import { useUi } from '../../stores/useUi.js';
const ui = useUi();
// useUi store is now handled by NucleoTable for tableBgColorAsistencias
const props = defineProps({
asistencias: {
@@ -70,12 +47,22 @@ const props = defineProps({
},
});
const emit = defineEmits(['edit']);
const emit = defineEmits(['edit']); // This component still emits 'edit' to its parent (AsistenciasIndex.vue)
const asistenciasStore = useAsistenciasStore();
const columns = [
{ key: 'id', label: 'ID' },
{ key: 'empleado_id', label: 'Empleado ID' },
{ key: 'entrada', label: 'Entrada' },
{ key: 'salida', label: 'Salida' },
{ key: 'estado', label: 'Estado' },
{ key: 'observacion', label: 'Observación' },
];
// Keep existing methods, they are used by the action buttons
const editAsistencia = (id) => {
emit('edit', id);
emit('edit', id); // Emitting to parent (AsistenciasIndex)
};
const confirmDeleteAsistencia = (asistencia) => {
@@ -87,7 +74,6 @@ const confirmDeleteAsistencia = (asistencia) => {
const deleteAsistenciaInternal = async (id) => {
try {
await asistenciasStore.deleteAsistencia(id);
// Optional: Show success notification or emit 'deleted' event
} catch (error) {
console.error(`Error deleting asistencia with id ${id}:`, error);
alert('Ocurrió un error al eliminar la asistencia.');

View File

@@ -1,57 +1,55 @@
<template>
<div :style="{ backgroundColor: ui.tableBgColorEmpleados }" class="shadow-md rounded-lg p-4 md:p-6 m-2 border border-gray-200 hover:shadow-lg transition-shadow duration-300 ease-in-out flex flex-col">
<div class="flex items-center mb-3 md:mb-4 pb-2 md:pb-3 border-b border-gray-100">
<img
:src="employee.avatar_url || 'https://via.placeholder.com/150'"
alt="Avatar del empleado"
class="w-16 h-16 rounded-full mr-4 border-2 object-cover"
:style="{ borderColor: 'var(--accent-color-empleados)' }"
/>
<div>
<h2 class="text-lg md:text-xl font-semibold" :style="{ color: 'var(--accent-color-empleados)' }">{{ employee.name }}</h2>
<p class="text-xs text-gray-500">ID: {{ employee.id }}</p>
<NucleoDataCard
:title="employee.name"
:accent-color="ui.accentColorEmpleados"
:background-color="ui.tableBgColorEmpleados"
:avatar-url="employee.avatar_url || 'https://via.placeholder.com/150'"
@edit="handleEdit"
@delete="confirmDeleteEmpleado"
>
<template #header>
<div class="flex items-center w-full">
<img
:src="employee.avatar_url || 'https://via.placeholder.com/150'"
alt="Avatar del empleado"
class="w-16 h-16 rounded-full mr-4 border-2 object-cover"
:style="{ borderColor: ui.accentColorEmpleados }"
/>
<div class="flex-grow">
<h2 class="text-lg md:text-xl font-semibold" :style="{ color: ui.accentColorEmpleados }">{{ employee.name }}</h2>
<p class="text-xs text-gray-500">ID: {{ employee.id }}</p>
</div>
</div>
</div>
<div class="text-sm text-gray-700 space-y-2">
<p v-if="employee.telefono" class="flex items-center">
<svg class="w-5 h-5 mr-2 shrink-0" :style="{ color: 'var(--accent-color-empleados)' }" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.308 1.154a11.042 11.042 0 005.516 5.516l1.154-2.308a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"></path></svg>
<strong class="font-medium text-gray-900">Teléfono:</strong>&nbsp;<span>{{ employee.telefono }}</span>
</p>
<p v-if="employee.ubicacion" class="flex items-center">
<svg class="w-5 h-5 mr-2 shrink-0" :style="{ color: 'var(--accent-color-empleados)' }" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
<strong class="font-medium text-gray-900">Ubicación:</strong>&nbsp;<span>{{ employee.ubicacion }}</span>
</p>
<p v-if="employee.cedula" class="flex items-center">
<svg class="w-5 h-5 mr-2 shrink-0" :style="{ color: 'var(--accent-color-empleados)' }" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V8a2 2 0 00-2-2h-5m-4 0V5a2 2 0 012-2h2a2 2 0 012 2v1m-4 0h4m-6 10v-5m0 5v0z"></path></svg>
<strong class="font-medium text-gray-900">Cédula:</strong>&nbsp;<span>{{ employee.cedula }}</span>
</p>
<p v-if="employee.grupo_estudio" class="flex items-center">
<svg class="w-5 h-5 mr-2 shrink-0" :style="{ color: 'var(--accent-color-empleados)' }" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 14l9-5-9-5-9 5 9 5z"></path><path d="M12 14l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-5.998 12.083 12.083 0 01.665-6.479L12 14z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 14l9-5-9-5-9 5 9 5zm0 0v3.945m0-3.945L6.161 10.58M17.839 10.58L12 14m5.839-3.42L12 14m0 0l6.161 3.42m-6.161-3.42L5.839 14.002"></path></svg>
<strong class="font-medium text-gray-900">Grupo Estudio:</strong>&nbsp;<span>{{ employee.grupo_estudio }}</span>
</p>
</div>
<div class="mt-auto pt-3 md:pt-4 flex justify-end space-x-2 md:space-x-3">
<button
@click="handleEdit"
class="px-3 py-1 md:px-4 md:py-2 text-xs md:text-sm font-medium rounded-md transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2 text-white"
:style="{ backgroundColor: 'var(--accent-color-empleados)', borderColor: 'var(--accent-color-empleados)' }"
@mouseover="buttonHover($event, true)"
@mouseleave="buttonHover($event, false)"
:class="`focus:ring-[var(--accent-color-empleados)]`"
>Editar</button>
<button
@click="confirmDeleteEmpleado"
class="px-3 py-1 md:px-4 md:py-2 text-xs md:text-sm font-medium rounded-md transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2 bg-red-600 hover:bg-red-700 text-white focus:ring-red-500"
>Eliminar</button>
</div>
</div>
</template>
<template #body>
<div class="text-sm text-gray-700 space-y-2">
<p v-if="employee.telefono" class="flex items-center">
<svg class="w-5 h-5 mr-2 shrink-0" :style="{ color: ui.accentColorEmpleados }" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.308 1.154a11.042 11.042 0 005.516 5.516l1.154-2.308a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"></path></svg>
<strong class="font-medium text-gray-900">Teléfono:</strong>&nbsp;<span>{{ employee.telefono }}</span>
</p>
<p v-if="employee.ubicacion" class="flex items-center">
<svg class="w-5 h-5 mr-2 shrink-0" :style="{ color: ui.accentColorEmpleados }" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
<strong class="font-medium text-gray-900">Ubicación:</strong>&nbsp;<span>{{ employee.ubicacion }}</span>
</p>
<p v-if="employee.cedula" class="flex items-center">
<svg class="w-5 h-5 mr-2 shrink-0" :style="{ color: ui.accentColorEmpleados }" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V8a2 2 0 00-2-2h-5m-4 0V5a2 2 0 012-2h2a2 2 0 012 2v1m-4 0h4m-6 10v-5m0 5v0z"></path></svg>
<strong class="font-medium text-gray-900">Cédula:</strong>&nbsp;<span>{{ employee.cedula }}</span>
</p>
<p v-if="employee.grupo_estudio" class="flex items-center">
<svg class="w-5 h-5 mr-2 shrink-0" :style="{ color: ui.accentColorEmpleados }" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 14l9-5-9-5-9 5 9 5z"></path><path d="M12 14l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-5.998 12.083 12.083 0 01.665-6.479L12 14z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 14l9-5-9-5-9 5 9 5zm0 0v3.945m0-3.945L6.161 10.58M17.839 10.58L12 14m5.839-3.42L12 14m0 0l6.161 3.42m-6.161-3.42L5.839 14.002"></path></svg>
<strong class="font-medium text-gray-900">Grupo Estudio:</strong>&nbsp;<span>{{ employee.grupo_estudio }}</span>
</p>
</div>
</template>
</NucleoDataCard>
</template>
<script setup lang="ts">
import { PropType } from 'vue'
import { useRouter } from 'vue-router'
import { defineProps, defineEmits } from 'vue'; // Removed PropType, useRouter as they are not used
import { useUi } from '../../stores/useUi.js';
import { useEmpleadosStore } from '../../stores/useEmpleados'; // Added import
import NucleoDataCard from '../ui/NucleoDataCard.vue';
const ui = useUi();
const emit = defineEmits(['edit']);
@@ -77,30 +75,22 @@ const confirmDeleteEmpleado = () => {
const deleteEmpleado = async () => {
try {
await empleadosStore.deleteEmpleado(props.employee.id);
// Optionally, show a success notification or emit an event if needed,
// though typically the list will update reactively from the store.
// Optionally, show a success notification
} catch (error) {
console.error('Error deleting employee:', error);
alert('Ocurrió un error al eliminar el empleado.');
}
};
const buttonHover = (event, isHovering) => {
const target = event.target;
if (isHovering) {
target.style.filter = 'brightness(90%)';
} else {
target.style.filter = 'brightness(100%)';
}
};
// buttonHover function removed as it's no longer used
</script>
<style scoped>
/* Minimal scoped styles */
.shrink-0 {
/* Scoped styles can be kept if they target elements within slots that NucleoDataCard doesn't style.
.shrink-0 might be useful if icons/elements in slots need it and it's not covered by Tailwind classes used.
.rounded-full with object-fit is good practice for avatars; NucleoDataCard's default avatar is also rounded.
The custom avatar styling (size, border) is now handled directly in the #header slot.
*/
.shrink-0 { /* Retained if needed by SVGs in slots, though Tailwind's `shrink-0` class can be used directly in template */
flex-shrink: 0;
}
.rounded-full {
object-fit: cover; /* Ensures the avatar image covers the area without distortion */
}
</style>

View File

@@ -1,86 +1,41 @@
<template>
<div class="p-4 sm:p-6 rounded-lg overflow-x-auto" :style="{ backgroundColor: ui.tableBgColorEmpleados }">
<table class="min-w-full divide-y divide-[var(--accent-color-empleados)]" :style="{ backgroundColor: ui.tableBgColorEmpleados }">
<thead class="divide-y divide-[var(--accent-color-empleados)]" :style="{ backgroundColor: ui.tableBgColorEmpleados }">
<tr>
<th scope="col" class="px-4 py-3 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 dark:text-slate-400 uppercase tracking-wider">
Avatar
</th>
<th scope="col" class="px-4 py-3 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 dark:text-slate-400 uppercase tracking-wider">
Nombre
</th>
<th scope="col" class="px-4 py-3 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 dark:text-slate-400 uppercase tracking-wider">
Cédula
</th>
<th scope="col" class="px-4 py-3 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 dark:text-slate-400 uppercase tracking-wider">
Teléfono
</th>
<th scope="col" class="px-4 py-3 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 dark:text-slate-400 uppercase tracking-wider">
Ubicación
</th>
<th scope="col" class="px-4 py-3 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 dark:text-slate-400 uppercase tracking-wider">
ID CIAT
</th>
<th scope="col" class="px-4 py-3 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 dark:text-slate-400 uppercase tracking-wider">
Acciones
</th>
</tr>
</thead>
<tbody class="divide-y divide-[var(--accent-color-empleados)]" :style="{ backgroundColor: ui.tableBgColorEmpleados }">
<tr v-if="!employees || employees.length === 0">
<td colspan="7" class="px-6 py-10 text-center text-gray-500 dark:text-slate-400 text-lg">
No hay empleados para mostrar.
</td>
</tr>
<tr v-for="employee in employees" :key="employee.id" class="transition-colors duration-150 ease-in-out hover:bg-[var(--accent-color-empleados)]/10 dark:hover:bg-[var(--accent-color-empleados)]/20">
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap">
<img
:src="employee.avatar_url || 'https://via.placeholder.com/40'"
alt="Avatar"
class="w-10 h-10 rounded-full object-cover border border-gray-300 shadow-sm"
/>
</td>
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm">
<div class="text-sm font-semibold text-gray-800 dark:text-slate-200">{{ employee.name }}</div>
<div v-if="employee.grupo_estudio" class="text-xs text-gray-500 dark:text-slate-400">
Grupo: {{ employee.grupo_estudio }}
</div>
</td>
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700 dark:text-slate-300">
{{ employee.cedula }}
</td>
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700 dark:text-slate-300">
{{ employee.telefono || '-' }}
</td>
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700 dark:text-slate-300">
{{ employee.ubicacion }}
</td>
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700 dark:text-slate-300">
{{ employee.idciat || '-' }}
</td>
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm font-medium">
<div class="flex items-center space-x-2">
<button @click="handleEdit(employee.id)" class="p-1.5 sm:p-2 rounded-md transition-all duration-150 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2 text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 hover:bg-blue-100 dark:hover:bg-blue-600/20 focus:ring-blue-500 dark:focus:ring-blue-400" title="Editar Empleado">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10" /></svg>
</button>
<button @click="confirmDeleteEmpleado(employee)" class="p-1.5 sm:p-2 rounded-md transition-all duration-150 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2 text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300 hover:bg-red-100 dark:hover:bg-red-600/20 focus:ring-red-500 dark:focus:ring-red-400" title="Eliminar">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12.56 0c1.153 0 2.24.03 3.22.077m3.22-.077L10.879 3.28a2.25 2.25 0 012.244-2.077h.093c.956 0 1.853.543 2.244 2.077L14.74 5.79m-4.858 0l-2.828-2.828A1.875 1.875 0 016.188 2.188l2.828 2.828m6.912 0l2.828-2.828a1.875 1.875 0 00-2.652-2.652L12 5.79M9.26 9h5.48L9.26 9z" /></svg>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<NucleoTable
:columns="columns"
:items="props.employees"
accent-color="--accent-color-empleados"
table-bg-color-name="tableBgColorEmpleados"
>
<template #cell-avatar_url="{ item }">
<img
:src="item.avatar_url || 'https://via.placeholder.com/40'"
alt="Avatar"
class="w-10 h-10 rounded-full object-cover border border-gray-300 dark:border-slate-600 shadow-sm"
/>
</template>
<template #cell-name="{ item }">
<div class="text-sm font-semibold text-gray-800 dark:text-slate-200">{{ item.name }}</div>
<div v-if="item.grupo_estudio" class="text-xs text-gray-500 dark:text-slate-400">
Grupo: {{ item.grupo_estudio }}
</div>
</template>
<template #actions="{ item }">
<div class="flex items-center space-x-2">
<button @click="handleEdit(item.id)" class="p-1.5 sm:p-2 rounded-md transition-all duration-150 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2 text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 hover:bg-blue-100 dark:hover:bg-blue-600/20 focus:ring-blue-500 dark:focus:ring-blue-400" title="Editar Empleado">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10" /></svg>
</button>
<button @click="confirmDeleteEmpleado(item)" class="p-1.5 sm:p-2 rounded-md transition-all duration-150 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2 text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300 hover:bg-red-100 dark:hover:bg-red-600/20 focus:ring-red-500 dark:focus:ring-red-400" title="Eliminar">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12.56 0c1.153 0 2.24.03 3.22.077m3.22-.077L10.879 3.28a2.25 2.25 0 012.244-2.077h.093c.956 0 1.853.543 2.244 2.077L14.74 5.79m-4.858 0l-2.828-2.828A1.875 1.875 0 016.188 2.188l2.828 2.828m6.912 0l2.828-2.828a1.875 1.875 0 00-2.652-2.652L12 5.79M9.26 9h5.48L9.26 9z" /></svg>
</button>
</div>
</template>
</NucleoTable>
</template>
<script setup>
import { useUi } from '../../stores/useUi.js';
import { defineProps, defineEmits } from 'vue';
import NucleoTable from '../ui/NucleoTable.vue'; // Import NucleoTable
import { useEmpleadosStore } from '../../stores/useEmpleados';
const ui = useUi();
const emit = defineEmits(['edit']);
const empleadosStore = useEmpleadosStore();
// useUi store is now handled by NucleoTable
const props = defineProps({
employees: {
@@ -90,6 +45,19 @@ const props = defineProps({
},
});
const emit = defineEmits(['edit']); // Emits 'edit' to parent (EmpleadosIndex.vue)
const empleadosStore = useEmpleadosStore();
const columns = [
{ key: 'avatar_url', label: 'Avatar' },
{ key: 'name', label: 'Nombre' },
{ key: 'cedula', label: 'Cédula' },
{ key: 'telefono', label: 'Teléfono' },
{ key: 'ubicacion', label: 'Ubicación' },
{ key: 'idciat', label: 'ID CIAT' },
];
// Keep existing methods
const handleEdit = (employeeId) => {
emit('edit', employeeId);
};
@@ -112,16 +80,7 @@ const deleteEmpleadoInternal = async (id) => {
<style scoped>
/* Scoped styles can be minimized or removed if Tailwind covers all needs */
.rounded-full {
object-fit: cover; /* Ensures avatar images are displayed nicely */
}
/* Optional: Keep icon transition if not handled by Tailwind's transition utilities on the button */
button svg {
transition: transform 0.15s ease-in-out;
}
button:hover svg {
transform: scale(1.1); /* Adjusted scale for a subtler effect */
}
/* .rounded-full { object-fit: cover; } */
/* button svg { transition: transform 0.15s ease-in-out; } */
/* button:hover svg { transform: scale(1.1); } */
</style>

View File

@@ -1,39 +1,29 @@
<template>
<div :style="{ backgroundColor: ui.tableBgColorPlanillas }" class="shadow-md rounded-lg p-4 md:p-6 m-2 border border-gray-200 hover:shadow-lg transition-shadow duration-300 ease-in-out flex flex-col">
<div class="flex justify-between items-center mb-3 md:mb-4 pb-2 md:pb-3 border-b border-gray-100">
<h4 class="text-lg md:text-xl font-semibold" :style="{ color: 'var(--accent-color-planillas)' }">Planilla ID: {{ planilla.id }}</h4>
<span :class="['px-2 py-1 text-xs font-bold text-white rounded-full', getStatusClass(planilla.estado)]">
{{ planilla.estado || 'N/A' }}
</span>
</div>
<div class="text-sm text-gray-700 space-y-2">
<p><strong class="font-medium text-gray-900">Título:</strong> {{ planilla.titulo }}</p>
<p><strong class="font-medium text-gray-900">Empleado ID:</strong> {{ planilla.empleado_id }}</p>
<p><strong class="font-medium text-gray-900">Desde:</strong> {{ formatDate(planilla.fecha_desde) }}</p>
<p><strong class="font-medium text-gray-900">Hasta:</strong> {{ formatDate(planilla.fecha_hasta) }}</p>
<p><strong class="font-medium text-gray-900">Total:</strong> {{ formatCurrency(planilla.total) }}</p>
</div>
<div class="mt-auto pt-3 md:pt-4 flex justify-end space-x-2 md:space-x-3">
<button
@click="editPlanilla"
class="px-3 py-1 md:px-4 md:py-2 text-xs md:text-sm font-medium rounded-md transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2 text-white"
:style="{ backgroundColor: 'var(--accent-color-planillas)', borderColor: 'var(--accent-color-planillas)' }"
@mouseover="buttonHover($event, true)"
@mouseleave="buttonHover($event, false)"
:class="`focus:ring-[var(--accent-color-planillas)]`"
>Editar</button>
<button
@click="confirmDeletePlanilla"
class="px-3 py-1 md:px-4 md:py-2 text-xs md:text-sm font-medium rounded-md transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2 bg-red-600 hover:bg-red-700 text-white focus:ring-red-500"
>Eliminar</button>
</div>
</div>
<NucleoDataCard
:title="`Planilla ID: ${planilla.id}`"
:status="planilla.estado"
:fields="cardFields"
:accent-color="ui.accentColorPlanillas"
:background-color="ui.tableBgColorPlanillas"
@edit="editPlanilla"
@delete="confirmDeletePlanilla"
>
<template #header>
<div class="flex items-center justify-between w-full">
<h2 class="text-xl font-semibold" :style="{ color: ui.accentColorPlanillas }">Planilla ID: {{ planilla.id }}</h2>
<span :class="['px-2 py-1 text-xs font-bold text-white rounded-full', getStatusClass(planilla.estado)]">
{{ planilla.estado || 'N/A' }}
</span>
</div>
</template>
</NucleoDataCard>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue';
import { defineProps, defineEmits, computed } from 'vue';
import { usePlanillasStore } from '../../stores/usePlanillas';
import { useUi } from '../../stores/useUi.js';
import NucleoDataCard from '../ui/NucleoDataCard.vue';
const ui = useUi();
@@ -56,23 +46,33 @@ const formatDate = (dateString) => {
const formatCurrency = (value) => {
if (value == null) return 'N/A';
return Number(value).toLocaleString('es-PY', {
style: 'currency',
currency: 'PYG'
return Number(value).toLocaleString('es-PY', {
style: 'currency',
currency: 'PYG',
minimumFractionDigits: 0, // Ensure no decimals for PYG if that's standard
maximumFractionDigits: 0,
});
};
const cardFields = computed(() => [
{ label: 'Título', value: props.planilla.titulo },
{ label: 'Empleado ID', value: props.planilla.empleado_id },
{ label: 'Desde', value: formatDate(props.planilla.fecha_desde) },
{ label: 'Hasta', value: formatDate(props.planilla.fecha_hasta) },
{ label: 'Total', value: formatCurrency(props.planilla.total) },
]);
const editPlanilla = () => {
emit('edit', props.planilla.id);
};
const confirmDeletePlanilla = () => {
if (confirm(`¿Está seguro de que desea eliminar la planilla "${props.planilla.titulo}" (ID: ${props.planilla.id})?`)) {
deletePlanilla();
deletePlanillaInternal();
}
};
const deletePlanilla = async () => {
const deletePlanillaInternal = async () => {
try {
await planillasStore.deletePlanilla(props.planilla.id);
} catch (error) {
@@ -82,28 +82,19 @@ const deletePlanilla = async () => {
};
const getStatusClass = (status) => {
if (!status) return 'bg-gray-400';
if (!status) return 'bg-gray-400 text-white';
const statusNormalized = status.toLowerCase().replace(/\s+/g, '-');
switch (statusNormalized) {
case 'pagado': return 'bg-green-500';
case 'pagado': return 'bg-green-500 text-white';
case 'pendiente': return 'bg-yellow-500 text-gray-800';
case 'anulado': return 'bg-red-500';
case 'borrador': return 'bg-gray-500';
default: return 'bg-gray-400';
case 'anulado': return 'bg-red-500 text-white';
case 'borrador': return 'bg-gray-500 text-white';
default: return 'bg-gray-400 text-white';
}
};
const buttonHover = (event, isHovering) => {
if (isHovering) {
event.target.style.filter = 'brightness(90%)';
} else {
event.target.style.filter = 'brightness(100%)';
}
};
// buttonHover removed
</script>
<style scoped>
/* All layout and most styling is now handled by Tailwind utility classes. */
/* Scoped styles can be used for very specific cases not covered by Tailwind, if any. */
/* Scoped styles can be removed or adjusted if not needed after refactor. */
</style>

View File

@@ -1,55 +1,43 @@
<template>
<div class="p-4 sm:p-6 rounded-lg overflow-x-auto" :style="{ backgroundColor: ui.tableBgColorPlanillas }">
<table class="min-w-full divide-y divide-[var(--accent-color-planillas)]" :style="{ backgroundColor: ui.tableBgColorPlanillas }">
<thead class="divide-y divide-[var(--accent-color-planillas)]" :style="{ backgroundColor: ui.tableBgColorPlanillas }">
<tr>
<th class="px-4 py-3 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 dark:text-slate-400 uppercase tracking-wider">ID</th>
<th class="px-4 py-3 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 dark:text-slate-400 uppercase tracking-wider">Título</th>
<th class="px-4 py-3 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 dark:text-slate-400 uppercase tracking-wider">Empleado ID</th>
<th class="px-4 py-3 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 dark:text-slate-400 uppercase tracking-wider">Fecha Desde</th>
<th class="px-4 py-3 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 dark:text-slate-400 uppercase tracking-wider">Fecha Hasta</th>
<th class="px-4 py-3 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 dark:text-slate-400 uppercase tracking-wider">Total</th>
<th class="px-4 py-3 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 dark:text-slate-400 uppercase tracking-wider">Estado</th>
<th class="px-4 py-3 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 dark:text-slate-400 uppercase tracking-wider">Acciones</th>
</tr>
</thead>
<tbody class="divide-y divide-[var(--accent-color-planillas)]" :style="{ backgroundColor: ui.tableBgColorPlanillas }">
<tr v-if="planillas && planillas.length === 0">
<td colspan="8" class="px-6 py-10 text-center text-gray-500 dark:text-slate-400 text-lg">No hay planillas para mostrar.</td>
</tr>
<tr v-for="planilla in planillas" :key="planilla.id" class="transition-colors duration-150 ease-in-out hover:bg-[var(--accent-color-planillas)]/10 dark:hover:bg-[var(--accent-color-planillas)]/20">
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700 dark:text-slate-300">{{ planilla.id }}</td>
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700 dark:text-slate-300">{{ planilla.titulo }}</td>
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700 dark:text-slate-300">{{ planilla.empleado_id }}</td>
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700 dark:text-slate-300">{{ formatDate(planilla.fecha_desde) }}</td>
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700 dark:text-slate-300">{{ formatDate(planilla.fecha_hasta) }}</td>
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700 dark:text-slate-300">{{ formatCurrency(planilla.total) }}</td>
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700 dark:text-slate-300">
<span :class="['px-2.5 py-0.5 rounded-full text-xs font-semibold', getStatusClass(planilla.estado)]">{{ planilla.estado }}</span>
</td>
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700 dark:text-slate-300">
<div class="flex items-center space-x-2">
<button @click="editPlanilla(planilla.id)" class="p-1.5 sm:p-2 rounded-md transition-all duration-150 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2 text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 hover:bg-blue-100 dark:hover:bg-blue-600/20 focus:ring-blue-500 dark:focus:ring-blue-400" title="Editar">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10" /></svg>
</button>
<button @click="confirmDeletePlanilla(planilla)" class="p-1.5 sm:p-2 rounded-md transition-all duration-150 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2 text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300 hover:bg-red-100 dark:hover:bg-red-600/20 focus:ring-red-500 dark:focus:ring-red-400" title="Eliminar">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12.56 0c1.153 0 2.24.03 3.22.077m3.22-.077L10.879 3.28a2.25 2.25 0 012.244-2.077h.093c.956 0 1.853.543 2.244 2.077L14.74 5.79m-4.858 0l-2.828-2.828A1.875 1.875 0 016.188 2.188l2.828 2.828m6.912 0l2.828-2.828a1.875 1.875 0 00-2.652-2.652L12 5.79M9.26 9h5.48L9.26 9z" /></svg>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<NucleoTable
:columns="columns"
:items="props.planillas"
accent-color="--accent-color-planillas"
table-bg-color-name="tableBgColorPlanillas"
>
<template #cell-fecha_desde="{ item }">
{{ formatDate(item.fecha_desde) }}
</template>
<template #cell-fecha_hasta="{ item }">
{{ formatDate(item.fecha_hasta) }}
</template>
<template #cell-total="{ item }">
{{ formatCurrency(item.total) }}
</template>
<template #cell-estado="{ item }">
<span :class="['px-2.5 py-0.5 rounded-full text-xs font-semibold', getStatusClass(item.estado)]">
{{ item.estado }}
</span>
</template>
<template #actions="{ item }">
<div class="flex items-center space-x-2">
<button @click="editPlanilla(item.id)" class="p-1.5 sm:p-2 rounded-md transition-all duration-150 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2 text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 hover:bg-blue-100 dark:hover:bg-blue-600/20 focus:ring-blue-500 dark:focus:ring-blue-400" title="Editar">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10" /></svg>
</button>
<button @click="confirmDeletePlanilla(item)" class="p-1.5 sm:p-2 rounded-md transition-all duration-150 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2 text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300 hover:bg-red-100 dark:hover:bg-red-600/20 focus:ring-red-500 dark:focus:ring-red-400" title="Eliminar">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12.56 0c1.153 0 2.24.03 3.22.077m3.22-.077L10.879 3.28a2.25 2.25 0 012.244-2.077h.093c.956 0 1.853.543 2.244 2.077L14.74 5.79m-4.858 0l-2.828-2.828A1.875 1.875 0 016.188 2.188l2.828 2.828m6.912 0l2.828-2.828a1.875 1.875 0 00-2.652-2.652L12 5.79M9.26 9h5.48L9.26 9z" /></svg>
</button>
</div>
</template>
</NucleoTable>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue';
import NucleoTable from '../ui/NucleoTable.vue'; // Import NucleoTable
import { usePlanillasStore } from '../../stores/usePlanillas';
import { formatDate, formatCurrency, getStatusClass } from '../../utils/formatters.js';
import { useUi } from '../../stores/useUi.js';
const ui = useUi();
// useUi store is now handled by NucleoTable
const props = defineProps({
planillas: {
@@ -59,10 +47,20 @@ const props = defineProps({
},
});
const emit = defineEmits(['edit']); // Removed 'delete' as it's handled internally
const emit = defineEmits(['edit']); // Emits 'edit' to parent (PlanillasIndex.vue)
const planillasStore = usePlanillasStore();
const columns = [
{ key: 'id', label: 'ID' },
{ key: 'titulo', label: 'Título' },
{ key: 'empleado_id', label: 'Empleado ID' },
{ key: 'fecha_desde', label: 'Fecha Desde' },
{ key: 'fecha_hasta', label: 'Fecha Hasta' },
{ key: 'total', label: 'Total' },
{ key: 'estado', label: 'Estado' },
];
// Keep existing methods
const editPlanilla = (id) => {
emit('edit', id);
};
@@ -76,8 +74,6 @@ const confirmDeletePlanilla = (planilla) => {
const deletePlanillaInternal = async (id) => {
try {
await planillasStore.deletePlanilla(id);
// Optional: Show success notification
// No need to emit 'delete' if the store handles list updates and parent components react to store changes
} catch (error) {
console.error(`Error deleting planilla with id ${id}:`, error);
// Optional: Show error notification

View File

@@ -1,42 +1,30 @@
<template>
<div :style="{ backgroundColor: ui.tableBgColorTareas }" class="shadow-md rounded-lg p-4 md:p-6 m-2 border border-gray-200 hover:shadow-lg transition-shadow duration-300 ease-in-out flex flex-col">
<div class="flex justify-between items-center mb-3 md:mb-4 pb-2 md:pb-3 border-b border-gray-100">
<h4 class="text-lg md:text-xl font-semibold" :style="{ color: 'var(--accent-color-tareas)' }">Tarea ID: {{ tarea.id }}</h4>
<span :class="['px-2 py-1 text-xs font-bold text-white rounded-full', getStatusClass(tarea.estado)]">
{{ tarea.estado || 'N/A' }}
</span>
</div>
<div class="text-sm text-gray-700 space-y-2">
<p><strong class="font-medium text-gray-900">Título:</strong> {{ tarea.titulo }}</p>
<p><strong class="font-medium text-gray-900">Empleado ID:</strong> {{ tarea.empleado_id }}</p>
<p><strong class="font-medium text-gray-900">Fecha:</strong> {{ formatDate(tarea.fecha) }}</p>
<p><strong class="font-medium text-gray-900">Tipo:</strong> {{ tarea.tipo || 'N/A' }}</p>
<p v-if="tarea.precio != null"><strong class="font-medium text-gray-900">Precio:</strong> {{ formatCurrency(tarea.precio) }}</p>
<p v-if="tarea.observacion" class="italic text-gray-600 bg-gray-50 p-2 border-l-3 rounded" :style="{ borderColor: 'var(--accent-color-tareas)' }">
<strong class="font-medium text-gray-900">Observación:</strong> {{ tarea.observacion }}
</p>
</div>
<div class="mt-auto pt-3 md:pt-4 flex justify-end space-x-2 md:space-x-3">
<button
@click="editTarea"
class="px-3 py-1 md:px-4 md:py-2 text-xs md:text-sm font-medium rounded-md transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2 text-white"
:style="{ backgroundColor: 'var(--accent-color-tareas)', borderColor: 'var(--accent-color-tareas)' }"
@mouseover="buttonHover($event, true)"
@mouseleave="buttonHover($event, false)"
:class="`focus:ring-[var(--accent-color-tareas)]`"
>Editar</button>
<button
@click="confirmDeleteTarea"
class="px-3 py-1 md:px-4 md:py-2 text-xs md:text-sm font-medium rounded-md transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2 bg-red-600 hover:bg-red-700 text-white focus:ring-red-500"
>Eliminar</button>
</div>
</div>
<NucleoDataCard
:title="`Tarea ID: ${tarea.id}`"
:status="tarea.estado"
:fields="cardFields"
:accent-color="ui.accentColorTareas"
:background-color="ui.tableBgColorTareas"
:observation="tarea.observacion"
@edit="editTarea"
@delete="confirmDeleteTarea"
>
<template #header>
<div class="flex items-center justify-between w-full">
<h2 class="text-xl font-semibold" :style="{ color: ui.accentColorTareas }">Tarea ID: {{ tarea.id }}</h2>
<span :class="['px-2 py-1 text-xs font-bold text-white rounded-full', getStatusClass(tarea.estado)]">
{{ tarea.estado || 'N/A' }}
</span>
</div>
</template>
</NucleoDataCard>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue';
import { useTareasStore } from '../../stores/useTareas';
import { defineProps, defineEmits, computed } from 'vue';
import { useTareasStore } from '../../stores/useTareas';
import { useUi } from '../../stores/useUi.js';
import NucleoDataCard from '../ui/NucleoDataCard.vue';
const ui = useUi();
@@ -54,17 +42,32 @@ const tareasStore = useTareasStore();
const formatDate = (dateString) => {
if (!dateString) return 'N/A';
const date = new Date(dateString);
return date.toLocaleString('es-HN', { timeZone: 'America/Tegucigalpa', year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit' });
return date.toLocaleString('es-HN', { timeZone: 'America/Tegucigalpa', year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: true });
};
const formatCurrency = (value) => {
if (value == null) return '';
if (value == null) return ''; // Return empty string if no price, so label doesn't show with 'N/A'
return Number(value).toLocaleString('es-PY', {
style: 'currency',
currency: 'PYG',
currency: 'PYG',
minimumFractionDigits: 0,
maximumFractionDigits: 0,
});
};
const cardFields = computed(() => {
const fields = [
{ label: 'Título', value: props.tarea.titulo },
{ label: 'Empleado ID', value: props.tarea.empleado_id },
{ label: 'Fecha', value: formatDate(props.tarea.fecha) },
{ label: 'Tipo', value: props.tarea.tipo || 'N/A' },
];
if (props.tarea.precio != null) {
fields.push({ label: 'Precio', value: formatCurrency(props.tarea.precio) });
}
return fields;
});
const editTarea = () => {
emit('edit', props.tarea.id);
};
@@ -85,35 +88,25 @@ const deleteTareaInternal = async () => {
};
const getStatusClass = (status) => {
if (!status) return 'bg-gray-400';
if (!status) return 'bg-gray-400 text-white';
const statusNormalized = status.toLowerCase().replace(/\s+/g, '-');
switch (statusNormalized) {
case 'pendiente': return 'bg-yellow-500 text-gray-800';
case 'realizada':
case 'completada':
case 'hecho': return 'bg-green-500';
case 'en-progreso': return 'bg-blue-500';
case 'hecho': return 'bg-green-500 text-white';
case 'en-progreso': return 'bg-blue-500 text-white';
case 'anulada':
case 'cancelada': return 'bg-red-500';
case 'archivada': return 'bg-gray-500';
default: return 'bg-gray-400';
case 'cancelada': return 'bg-red-500 text-white';
case 'archivada': return 'bg-gray-500 text-white';
default: return 'bg-gray-400 text-white';
}
};
const buttonHover = (event, isHovering) => {
if (isHovering) {
event.target.style.filter = 'brightness(90%)';
} else {
event.target.style.filter = 'brightness(100%)';
}
};
// buttonHover removed
</script>
<style scoped>
/* Minimal scoped styles, mainly for complex status colors if not handled by Tailwind directly */
/* Or for :style bindings if preferred for dynamic properties like accent colors */
.border-l-3 {
border-left-width: 3px;
}
/* Scoped styles can be removed if NucleoDataCard's observation styling is acceptable.
The .border-l-3 class is for the old observation styling.
*/
</style>

View File

@@ -1,113 +1,80 @@
<template>
<div
class="p-4 sm:p-6 rounded-lg overflow-x-auto"
:style="{ backgroundColor: ui.tableBgColorTareas, '--bg-tareas': ui.tableBgColorTareas }"
<NucleoTable
:columns="columns"
:items="props.tareas"
accent-color="--accent-color-tareas"
table-bg-color-name="tableBgColorTareas"
>
<table
class="min-w-full divide-y divide-[var(--accent-color-tareas)]"
:style="{ backgroundColor: ui.tableBgColorTareas }"
>
<thead
class="divide-y divide-[var(--accent-color-tareas)]"
:style="{ backgroundColor: ui.tableBgColorTareas }"
>
<tr>
<th class="px-4 py-3 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 dark:text-slate-400 uppercase tracking-wider">ID</th>
<th class="px-4 py-3 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 dark:text-slate-400 uppercase tracking-wider">Título</th>
<th class="px-4 py-3 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 dark:text-slate-400 uppercase tracking-wider">Empleado ID</th>
<th class="px-4 py-3 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 dark:text-slate-400 uppercase tracking-wider">Fecha</th>
<th class="px-4 py-3 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 dark:text-slate-400 uppercase tracking-wider">Estado</th>
<th class="px-4 py-3 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 dark:text-slate-400 uppercase tracking-wider">Tipo</th>
<th class="px-4 py-3 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 dark:text-slate-400 uppercase tracking-wider">Precio</th>
<th class="px-4 py-3 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 dark:text-slate-400 uppercase tracking-wider">Planilla ID</th>
<th class="px-4 py-3 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 dark:text-slate-400 uppercase tracking-wider">Acciones</th>
</tr>
</thead>
<tbody
class="divide-y divide-[var(--accent-color-tareas)]"
:style="{ backgroundColor: ui.tableBgColorTareas }"
>
<tr v-if="!tareas || tareas.length === 0">
<td colspan="9" class="px-6 py-10 text-center text-gray-500 dark:text-slate-400 text-lg">
No hay tareas para mostrar.
</td>
</tr>
<tr
v-for="tarea in tareas"
:key="tarea.id"
class="bg-[var(--bg-tareas)] transition-colors duration-150 ease-in-out hover:bg-[var(--accent-color-tareas)]/10 dark:hover:bg-[var(--accent-color-tareas)]/20"
>
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700 dark:text-slate-300">{{ tarea.id }}</td>
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700 dark:text-slate-300">{{ tarea.titulo }}</td>
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700 dark:text-slate-300">{{ tarea.empleado_id }}</td>
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700 dark:text-slate-300">{{ formatDate(tarea.fecha) }}</td>
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700 dark:text-slate-300">
<span :class="['px-2.5 py-0.5 rounded-full text-xs font-semibold', getStatusClass(tarea.estado)]">
{{ tarea.estado }}
</span>
</td>
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700 dark:text-slate-300">{{ tarea.tipo || 'N/A' }}</td>
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700 dark:text-slate-300">{{ tarea.precio != null ? formatCurrency(tarea.precio) : 'N/A' }}</td>
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700 dark:text-slate-300">{{ tarea.planilla_id || 'N/A' }}</td>
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700 dark:text-slate-300">
<div class="flex items-center space-x-2">
<button
@click="editTarea(tarea.id)"
class="p-1.5 sm:p-2 rounded-md transition-all duration-150 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2 text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 hover:bg-blue-100 dark:hover:bg-blue-600/20 focus:ring-blue-500 dark:focus:ring-blue-400"
title="Editar"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zM16.862 4.487L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10" />
</svg>
</button>
<button
@click="confirmDeleteTarea(tarea)"
class="p-1.5 sm:p-2 rounded-md transition-all duration-150 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2 text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300 hover:bg-red-100 dark:hover:bg-red-600/20 focus:ring-red-500 dark:focus:ring-red-400"
title="Eliminar"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166M18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79M19.228 5.79a48.108 48.108 0 00-3.478-.397M3.19 5.393c1.153 0 2.24.03 3.22.077m3.22-.077L10.879 3.28A2.25 2.25 0 0113.123 1.203h.093a2.25 2.25 0 012.244 2.077L14.74 5.79M9.882 5.79L7.054 2.962A1.875 1.875 0 016.188 2.188L9.016 5.016M15.928 5.79L18.756 2.962a1.875 1.875 0 00-2.652-2.652L12 5.79" />
</svg>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<template #cell-fecha="{ item }">
{{ formatDate(item.fecha) }}
</template>
<template #cell-estado="{ item }">
<span :class="['px-2.5 py-0.5 rounded-full text-xs font-semibold', getStatusClass(item.estado)]">
{{ item.estado }}
</span>
</template>
<template #cell-tipo="{ item }">
{{ item.tipo || 'N/A' }}
</template>
<template #cell-precio="{ item }">
{{ item.precio != null ? formatCurrency(item.precio) : 'N/A' }}
</template>
<template #cell-planilla_id="{ item }">
{{ item.planilla_id || 'N/A' }}
</template>
<template #actions="{ item }">
<div class="flex items-center space-x-2">
<button @click="editTarea(item.id)" class="p-1.5 sm:p-2 rounded-md transition-all duration-150 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2 text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 hover:bg-blue-100 dark:hover:bg-blue-600/20 focus:ring-blue-500 dark:focus:ring-blue-400" title="Editar">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10" /></svg>
</button>
<button @click="confirmDeleteTarea(item)" class="p-1.5 sm:p-2 rounded-md transition-all duration-150 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2 text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300 hover:bg-red-100 dark:hover:bg-red-600/20 focus:ring-red-500 dark:focus:ring-red-400" title="Eliminar">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12.56 0c1.153 0 2.24.03 3.22.077m3.22-.077L10.879 3.28a2.25 2.25 0 012.244-2.077h.093c.956 0 1.853.543 2.244 2.077L14.74 5.79m-4.858 0l-2.828-2.828A1.875 1.875 0 016.188 2.188l2.828 2.828m6.912 0l2.828-2.828a1.875 1.875 0 00-2.652-2.652L12 5.79M9.26 9h5.48L9.26 9z" /></svg>
</button>
</div>
</template>
</NucleoTable>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue'
import { useTareasStore } from '@/stores/useTareas'
import { formatDate, formatCurrency, getStatusClass } from '@/utils/formatters'
import { useUi } from '@/stores/useUi'
import { defineProps, defineEmits } from 'vue';
import NucleoTable from '../ui/NucleoTable.vue'; // Import NucleoTable
import { useTareasStore } from '../../stores/useTareas';
import { formatDate, formatCurrency, getStatusClass } from '../../utils/formatters.js';
// useUi store is now handled by NucleoTable
const ui = useUi()
defineProps({
const props = defineProps({
tareas: { type: Array, required: true, default: () => [] },
})
});
const emit = defineEmits(['edit'])
const tareasStore = useTareasStore()
const emit = defineEmits(['edit']); // Emits 'edit' to parent (TareasIndex.vue)
const tareasStore = useTareasStore();
const editTarea = (id) => emit('edit', id)
const columns = [
{ key: 'id', label: 'ID' },
{ key: 'titulo', label: 'Título' },
{ key: 'empleado_id', label: 'Empleado ID' },
{ key: 'fecha', label: 'Fecha' },
{ key: 'estado', label: 'Estado' },
{ key: 'tipo', label: 'Tipo' },
{ key: 'precio', label: 'Precio' },
{ key: 'planilla_id', label: 'Planilla ID' },
];
// Keep existing methods
const editTarea = (id) => emit('edit', id);
const confirmDeleteTarea = (tarea) => {
if (confirm(`¿Está seguro de que desea eliminar la tarea "${tarea.titulo}" (ID: ${tarea.id})?`))
deleteTareaInternal(tarea.id)
}
if (confirm(`¿Está seguro de que desea eliminar la tarea "${tarea.titulo}" (ID: ${tarea.id})?`)) {
deleteTareaInternal(tarea.id);
}
};
const deleteTareaInternal = async (id) => {
try {
await tareasStore.deleteTarea(id)
await tareasStore.deleteTarea(id);
} catch (e) {
console.error(e)
alert('Ocurrió un error al eliminar la tarea.')
console.error('Error deleting tarea:', e);
alert('Ocurrió un error al eliminar la tarea.');
}
}
};
</script>

View File

@@ -0,0 +1,89 @@
<template>
<div
class="rounded-lg shadow-md p-6 border-t-4"
:style="{ backgroundColor: backgroundColor, borderTopColor: accentColor }"
>
<!-- Header -->
<div class="flex items-center justify-between mb-4">
<slot name="header">
<div class="flex items-center">
<img v-if="avatarUrl" :src="avatarUrl" alt="Avatar" class="w-10 h-10 rounded-full mr-3">
<h2 class="text-xl font-semibold text-gray-800">{{ title }}</h2>
</div>
<span v-if="status" :class="`px-2 py-1 text-xs font-semibold leading-tight text-${accentColor}-700 bg-${accentColor}-100 rounded-full`">{{ status }}</span>
</slot>
</div>
<!-- Body -->
<div class="mb-4">
<slot name="body">
<div v-for="(field, index) in fields" :key="index" class="mb-2">
<p class="text-sm text-gray-600 font-medium">{{ field.label }}:</p>
<p class="text-sm text-gray-800">{{ field.value }}</p>
</div>
<div v-if="observation" class="mt-4">
<p class="text-sm text-gray-600 font-medium">Observation:</p>
<p class="text-sm text-gray-800">{{ observation }}</p>
</div>
</slot>
</div>
<!-- Footer -->
<div class="flex justify-end items-center">
<slot name="footer">
<slot name="actions"></slot>
<button v-if="showEditButton" @click="$emit('edit')" class="text-sm text-blue-500 hover:text-blue-700 font-medium mr-4">Edit</button>
<button v-if="showDeleteButton" @click="$emit('delete')" class="text-sm text-red-500 hover:text-red-700 font-medium">Delete</button>
</slot>
</div>
</div>
</template>
<script>
export default {
name: 'NucleoDataCard',
props: {
title: {
type: String,
required: true,
},
status: {
type: String,
default: '',
},
fields: {
type: Array,
default: () => [], // Example: [{ label: 'ID', value: '123' }]
},
accentColor: {
type: String,
default: '#808080', // Default to gray hex
},
backgroundColor: {
type: String,
default: '#FFFFFF', // Default to white hex
},
avatarUrl: {
type: String,
default: '',
},
observation: {
type: String,
default: '',
},
showEditButton: {
type: Boolean,
default: true,
},
showDeleteButton: {
type: Boolean,
default: true,
},
},
emits: ['edit', 'delete'],
};
</script>
<style scoped>
/* Scoped styles can be added here if needed, Tailwind CSS is preferred for general styling */
</style>

View File

@@ -0,0 +1,64 @@
<template>
<div class="p-4 sm:p-6 rounded-lg overflow-x-auto" :style="{ backgroundColor: tableBgColor }">
<table class="min-w-full divide-y" :class="[`divide-[var(${props.accentColor})]`]" :style="{ backgroundColor: tableBgColor }">
<thead :class="[`divide-y divide-[var(${props.accentColor})]`]" :style="{ backgroundColor: tableBgColor }">
<tr>
<th v-for="column in columns" :key="column.key" class="px-4 py-3 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 dark:text-slate-400 uppercase tracking-wider">
{{ column.label }}
</th>
<th v-if="hasActionSlot" class="px-4 py-3 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 dark:text-slate-400 uppercase tracking-wider">
Acciones
</th>
</tr>
</thead>
<tbody class="divide-y" :class="[`divide-[var(${props.accentColor})]`]" :style="{ backgroundColor: tableBgColor }">
<tr v-if="!items || items.length === 0">
<td :colspan="columns.length + (hasActionSlot ? 1 : 0)" class="px-6 py-10 text-center text-gray-500 dark:text-slate-400 text-lg">
No hay datos para mostrar.
</td>
</tr>
<tr
v-for="(item, index) in items"
:key="item.id || index"
class="transition-colors duration-150 ease-in-out"
:class="[`hover:bg-[var(${props.accentColor})]/10 dark:hover:bg-[var(${props.accentColor})]/20`]"
>
<td v-for="column in columns" :key="column.key" class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700 dark:text-slate-300">
<slot :name="`cell-${column.key}`" :item="item" :index="index">
{{ item[column.key] }}
</slot>
</td>
<td v-if="hasActionSlot" class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm">
<slot name="actions" :item="item" :index="index"></slot>
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script setup>
import { defineProps, defineEmits, computed, useSlots } from 'vue';
import { useUi } from '../../stores/useUi';
const props = defineProps({
columns: { type: Array, required: true, default: () => [] },
items: { type: Array, required: true, default: () => [] },
accentColor: { type: String, required: true }, // Example: '--accent-color-empleados'
tableBgColorName: { type: String, required: true }, // Example: 'tableBgColorEmpleados'
});
const emit = defineEmits(['editItem', 'deleteItem']); // Define the emits that can be triggered
const ui = useUi();
const slots = useSlots();
const tableBgColor = computed(() => {
return ui[props.tableBgColorName] || 'transparent'; // Default to transparent if not found
});
const hasActionSlot = computed(() => {
// Check if the actions slot has content
return !!slots.actions;
});
</script>

View File

@@ -0,0 +1,96 @@
# UI Components
This directory contains general-purpose UI components used throughout the application.
## NucleoDataCard.vue
A flexible and centralized card component for displaying data entities.
### Purpose
The `NucleoDataCard` component provides a standardized way to display information in a card format. It's designed to be adaptable to various data types and styling requirements through props and slots. It supports a header (with optional avatar, title, and status), a body for displaying data fields and an optional observation section, and a footer with action buttons.
### Props
| Prop | Type | Default | Description |
|---------------------|---------|-------------|-----------------------------------------------------------------------------|
| `title` | String | _Required_ | The main title of the card, typically displayed in the header. |
| `status` | String | `''` | A status string to be displayed as a badge in the header. |
| `fields` | Array | `[]` | An array of objects (`{ label: String, value: String }`) for data rows. Each object represents a piece of data to be displayed with a label and its value. |
| `accentColor` | String | `'#808080'` | Hex color code for the card's top border. |
| `backgroundColor` | String | `'#FFFFFF'` | Hex color code for the card's background. |
| `avatarUrl` | String | `''` | URL for an avatar image to be displayed in the header. |
| `observation` | String | `''` | A text string for an observation section displayed below the fields. |
| `showEditButton` | Boolean | `true` | Whether to display the default Edit button in the footer. |
| `showDeleteButton` | Boolean | `true` | Whether to display the default Delete button in the footer. |
*Note: `accentColor` and `backgroundColor` are applied using inline styles. The default status badge styling in the header slot might not work as expected with hex `accentColor` values for its default Tailwind classes; use the `header` slot for custom status styling if needed.*
### Slots
- **`header`**: Allows custom content or layout for the card header. If used, it overrides the default header structure which includes the avatar, title, and status badge.
- **`body`**: Allows complete customization of the main card content area. If used, you are responsible for rendering the content that would normally go here, such as iterating through the `fields` prop and displaying the `observation` prop. The default content of this slot handles the rendering of `fields` and `observation`.
- **`footer`**: Allows custom content to be injected at the beginning of the card footer, before the `actions` slot and default Edit/Delete buttons.
- **`actions`**: Allows adding custom buttons or other interactive elements to the footer. These are rendered before the default Edit and Delete buttons if they are visible.
### Events
- **`edit`**: Emitted with no payload when the default Edit button (if shown) is clicked.
- **`delete`**: Emitted with no payload when the default Delete button (if shown) is clicked.
### Example Usage
```vue
<template>
<NucleoDataCard
title="John Doe"
status="Active"
:fields="userDetails"
accentColor="#3498db"
backgroundColor="#f9f9f9"
avatarUrl="path/to/avatar.jpg"
observation="This user has been active for 3 years."
:showEditButton="true"
:showDeleteButton="true"
@edit="onEditUser"
@delete="onDeleteUser"
>
<template #actions>
<button @click="viewMoreDetails" class="px-3 py-1 text-sm text-white bg-blue-500 rounded hover:bg-blue-600">
View More
</button>
</template>
</NucleoDataCard>
</template>
<script setup>
import NucleoDataCard from './NucleoDataCard.vue'; // Adjust path as needed
import { ref } from 'vue';
const userDetails = ref([
{ label: 'Email', value: 'john.doe@example.com' },
{ label: 'Department', value: 'Engineering' },
{ label: 'Role', value: 'Software Developer' },
]);
const onEditUser = () => {
console.log('Edit user event triggered');
// Add logic to handle user editing
};
const onDeleteUser = () => {
console.log('Delete user event triggered');
// Add logic to handle user deletion
};
const viewMoreDetails = () => {
console.log('View more details action triggered');
// Add logic for custom action
};
</script>
<style scoped>
/* Example of additional styling if needed for custom elements via slots */
/* Tailwind CSS classes are generally preferred */
</style>
```