Merge remote-tracking branch 'github/main'
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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.');
|
||||
|
||||
@@ -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> <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> <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> <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> <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> <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> <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> <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> <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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
89
ui/src/components/ui/NucleoDataCard.vue
Normal file
89
ui/src/components/ui/NucleoDataCard.vue
Normal 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>
|
||||
64
ui/src/components/ui/NucleoTable.vue
Normal file
64
ui/src/components/ui/NucleoTable.vue
Normal 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>
|
||||
96
ui/src/components/ui/README.md
Normal file
96
ui/src/components/ui/README.md
Normal 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>
|
||||
```
|
||||
Reference in New Issue
Block a user