Merge pull request #24 from josedario87/feature/nucleo-data-card
Feature/nucleo data card
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>
|
||||
<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>
|
||||
<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>
|
||||
</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,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">
|
||||
<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: 'var(--accent-color-empleados)' }"
|
||||
:style="{ borderColor: ui.accentColorEmpleados }"
|
||||
/>
|
||||
<div>
|
||||
<h2 class="text-lg md:text-xl font-semibold" :style="{ color: 'var(--accent-color-empleados)' }">{{ employee.name }}</h2>
|
||||
<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>
|
||||
</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: '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>
|
||||
<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: '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>
|
||||
<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: '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>
|
||||
<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: '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>
|
||||
<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>
|
||||
<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>
|
||||
</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,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>
|
||||
<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>
|
||||
<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>
|
||||
</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();
|
||||
|
||||
@@ -58,21 +48,31 @@ const formatCurrency = (value) => {
|
||||
if (value == null) return 'N/A';
|
||||
return Number(value).toLocaleString('es-PY', {
|
||||
style: 'currency',
|
||||
currency: 'PYG'
|
||||
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,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>
|
||||
<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>
|
||||
<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>
|
||||
</template>
|
||||
</NucleoDataCard>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps, defineEmits } from 'vue';
|
||||
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',
|
||||
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>
|
||||
|
||||
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>
|
||||
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