Refactor: Standardize UI table components and update README

This commit introduces a standardized approach to table components within the UI.

Key changes include:
- Refactored tablaAsistencias.vue, tablaEmpleados.vue, tablaPlanillas.vue, and tablaTareas.vue to use Tailwind CSS for consistent styling.
- Defined and applied common Tailwind utility patterns for table structure (container, header, body, rows, cells), action buttons (with SVG icons), and status indicators (badges).
- Created a shared utility file at `ui/src/utils/formatters.js` for common functions like date formatting, currency formatting, text truncation, and status class generation, reducing code duplication.
- Updated table components to use these shared utility functions.
- Updated `ui/README.md` to document the UI modules, the standardized table structure, styling conventions, and usage of utility functions.

This standardization enhances code maintainability, improves consistency in the user interface, and provides clear guidelines for future table component development.
This commit is contained in:
google-labs-jules[bot]
2025-05-31 07:18:19 +00:00
parent 8fd1ba672e
commit 9fd4f30016
6 changed files with 273 additions and 434 deletions

View File

@@ -1,5 +1,52 @@
# Vue 3 + Vite
# Project UI (Vue 3 + Vite)
This project frontend is built with Vue 3 and Vite. It provides a user interface for managing various aspects of the application.
## Development
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
## UI Structure
The UI is organized into several modules, each handling a specific domain of the application. Common UI elements, like tables, have been standardized for a consistent look and feel.
## UI Modules
The application currently includes the following primary UI modules:
* **Asistencias (Attendance):** Manages attendance records.
* **Empleados (Employees):** Manages employee information.
* **Planillas (Payrolls/Sheets):** Manages payrolls or general data sheets.
* **Tareas (Tasks):** Manages tasks and assignments.
Each module typically provides views for listing, creating, and editing items within its domain.
## Standardized Table Components
To ensure consistency across the application, table components used for displaying lists of data (e.g., `tablaAsistencias.vue`, `tablaEmpleados.vue`) have been standardized.
**Key Features & Standards:**
* **Styling:** Tables are styled using **Tailwind CSS**. This utility-first CSS framework allows for flexible and consistent styling.
* **Structure:**
* **Container:** Tables are wrapped in a `div` with classes `p-4 sm:p-6 bg-white shadow-md rounded-lg overflow-x-auto` for padding, background, shadow, rounded corners, and horizontal scrolling on smaller screens.
* **Table:** The `<table>` element uses `min-w-full divide-y divide-gray-200`.
* **Header (`<thead>`):** Styled with `bg-gray-50`. Header cells (`<th>`) use `px-4 py-3 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider`.
* **Body (`<tbody>`):** Uses `bg-white divide-y divide-gray-200`. Data rows (`<tr>`) have a hover effect (`hover:bg-gray-100`). Data cells (`<td>`) use `px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700`.
* **No Data Message:** A standardized message "No hay [items] para mostrar." is displayed if the table is empty, styled with `px-6 py-10 text-center text-gray-500 text-lg`.
* **Action Buttons:**
* Common actions like "Edit" and "Delete" are standardized.
* Buttons use SVG icons (Heroicons outline style) and consistent Tailwind CSS classes for styling and hover/focus states (e.g., `text-blue-600 hover:text-blue-800` for edit, `text-red-600 hover:text-red-800` for delete).
* Actions are typically grouped in a `div` with `flex items-center space-x-2`.
* **Status Indicators:**
* Status fields (e.g., "Estado") are visually represented using badges with consistent styling (`px-2.5 py-0.5 rounded-full text-xs font-semibold` along with color classes like `bg-green-100 text-green-800`).
* **Utility Functions:**
* Common data formatting (dates, currency, text truncation) and status styling logic is centralized in `ui/src/utils/formatters.js`. Components import and use these functions to maintain consistency and reduce code duplication.
* **Theming:**
* While the overall table structure is standardized, module-specific accent colors (defined as CSS variables like `var(--accent-color-module)`) are used in the parent views (e.g., `AsistenciasIndex.vue`) for elements like page titles and "Create New" buttons. This allows some thematic distinction per module.
* **Creating New Tables:**
* When adding new tables to the UI, they should adhere to these established standards to maintain a cohesive user experience. Refer to existing components like `tablaAsistencias.vue` or `tablaPlanillas.vue` as a template.
This standardization aims to improve code maintainability, reduce redundancy, and provide a consistent and predictable user interface.

View File

@@ -1,31 +1,39 @@
<template>
<div class="tabla-asistencias-container">
<table class="tabla-asistencias">
<thead>
<div class="p-4 sm:p-6 bg-white shadow-md rounded-lg overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th>ID</th>
<th>Empleado ID</th>
<th>Entrada</th>
<th>Salida</th>
<th>Estado</th>
<th>Observación</th>
<th>Acciones</th>
<th class="px-4 py-3 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 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 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 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 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 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 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 uppercase tracking-wider">Acciones</th>
</tr>
</thead>
<tbody>
<tbody class="bg-white divide-y divide-gray-200">
<tr v-if="!asistencias || asistencias.length === 0">
<td :colspan="7" style="text-align: center;">No hay asistencias para mostrar.</td>
<td colspan="7" class="px-6 py-10 text-center text-gray-500 text-lg">No hay asistencias para mostrar.</td>
</tr>
<tr v-for="asistencia in asistencias" :key="asistencia.id">
<td>{{ asistencia.id }}</td>
<td>{{ asistencia.empleado_id }}</td>
<td>{{ formatDateTime(asistencia.entrada) }}</td>
<td>{{ asistencia.salida ? formatDateTime(asistencia.salida) : 'N/A' }}</td>
<td><span :class="`estado-${asistencia.estado?.toLowerCase().replace(/\s+/g, '-')}`">{{ asistencia.estado }}</span></td>
<td :title="asistencia.observacion">{{ truncateText(asistencia.observacion, 50) }}</td>
<td>
<button @click="editAsistencia(asistencia.id)" class="action-button edit-button">Editar</button>
<button @click="confirmDeleteAsistencia(asistencia)" class="action-button delete-button">Eliminar</button>
<tr v-for="asistencia in asistencias" :key="asistencia.id" class="hover:bg-gray-100 transition-colors duration-150 ease-in-out">
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700">{{ asistencia.id }}</td>
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700">{{ asistencia.empleado_id }}</td>
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700">{{ formatDateTime(asistencia.entrada) }}</td>
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700">{{ 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">
<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 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">
<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 hover:text-blue-800 hover:bg-blue-100 focus:ring-blue-500" 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 hover:text-red-800 hover:bg-red-100 focus:ring-red-500" 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>
@@ -36,6 +44,7 @@
<script setup>
import { defineProps, defineEmits } from 'vue';
import { useAsistenciasStore } from '../../stores/useAsistencias';
import { formatDateTime, truncateText, getStatusClass } from '../../utils/formatters.js';
const props = defineProps({
asistencias: {
@@ -49,24 +58,6 @@ const emit = defineEmits(['edit']);
const asistenciasStore = useAsistenciasStore();
const formatDateTime = (dateTimeString) => {
if (!dateTimeString) return 'N/A';
const date = new Date(dateTimeString);
// Using UTC methods to ensure consistency if dates are stored in UTC
const day = String(date.getUTCDate()).padStart(2, '0');
const month = String(date.getUTCMonth() + 1).padStart(2, '0'); // Months are 0-indexed
const year = date.getUTCFullYear();
const hours = String(date.getUTCHours()).padStart(2, '0');
const minutes = String(date.getUTCMinutes()).padStart(2, '0');
return `${day}/${month}/${year} ${hours}:${minutes}`;
};
const truncateText = (text, maxLength) => {
if (!text) return 'N/A';
if (text.length <= maxLength) return text;
return text.substring(0, maxLength) + '...';
};
const editAsistencia = (id) => {
emit('edit', id);
};
@@ -87,79 +78,3 @@ const deleteAsistenciaInternal = async (id) => {
}
};
</script>
<style scoped>
.tabla-asistencias-container {
overflow-x: auto;
}
.tabla-asistencias {
/* Apply module-specific background color */
background-color: var(--table-bg-color-asistencias);
width: 100%;
border-collapse: collapse;
margin-top: 1em;
font-size: 0.9em;
}
.tabla-asistencias th,
.tabla-asistencias td {
border: 1px solid #ddd;
padding: 10px;
text-align: left;
vertical-align: middle; /* Good for table cells */
}
.tabla-asistencias th {
background-color: #f4f6f8; /* Light grey for header */
font-weight: 600; /* Bolder text for header */
color: #333;
}
.tabla-asistencias tr:nth-child(even) {
background-color: #f9fafb; /* Very light alternating row color */
}
.tabla-asistencias tr:hover {
background-color: #f0f0f0; /* Hover effect */
}
.action-button {
padding: 6px 10px;
margin-right: 6px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.85em;
transition: background-color 0.2s ease, transform 0.1s ease;
}
.action-button:hover {
transform: translateY(-1px); /* Slight lift effect */
}
.edit-button {
background-color: var(--accent-color-asistencias);
color: white;
}
.edit-button:hover {
filter: brightness(0.9);
}
.delete-button {
background-color: var(--warning-color); /* Use warning color for delete */
color: white;
}
.delete-button:hover {
filter: brightness(0.9);
}
/* Estado specific styling (using text color for tables is often cleaner) */
.estado-pendiente { color: #ffc107; font-weight: bold; } /* Amber */
.estado-presente,
.estado-confirmada { color: #28a745; font-weight: bold; } /* Green */
.estado-ausente { color: #dc3545; font-weight: bold; } /* Red */
.estado-justificada { color: #17a2b8; font-weight: bold; } /* Info Blue */
.estado-cancelada,
.estado-anulada { color: #6c757d; font-weight: bold; } /* Gray */
/* If you prefer background colors like in cards, copy those styles here, but they can be visually heavy in tables. */
</style>

View File

@@ -1,83 +1,72 @@
<template>
<div class="overflow-x-auto bg-white shadow-md rounded-lg p-4">
<table class="min-w-full divide-y divide-gray-200 table-empleados">
<div class="p-4 sm:p-6 bg-white shadow-md rounded-lg overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th scope="col" class="px-4 py-3 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Avatar
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th scope="col" class="px-4 py-3 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Nombre
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th scope="col" class="px-4 py-3 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Cédula
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th scope="col" class="px-4 py-3 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Teléfono
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th scope="col" class="px-4 py-3 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Ubicación
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th scope="col" class="px-4 py-3 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
ID CIAT
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th scope="col" class="px-4 py-3 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Acciones
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr v-if="!employees || employees.length === 0">
<td colspan="7" class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 text-center">
<td colspan="7" class="px-6 py-10 text-center text-gray-500 text-lg">
No hay empleados para mostrar.
</td>
</tr>
<tr v-for="employee in employees" :key="employee.id" class="hover:bg-gray-100 transition-colors duration-150 ease-in-out">
<td class="px-6 py-4 whitespace-nowrap">
<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-6 py-4 whitespace-nowrap">
<div class="text-sm font-semibold text-gray-900">{{ employee.name }}</div>
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700">
<div class="text-sm font-semibold text-gray-800">{{ employee.name }}</div>
<div v-if="employee.grupo_estudio" class="text-xs text-gray-500">
Grupo: {{ employee.grupo_estudio }}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700">
{{ employee.cedula }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700">
{{ employee.telefono || '-' }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700">
{{ employee.ubicacion }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700">
{{ employee.idciat || '-' }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium space-x-2">
<button
@click="handleEdit(employee.id)"
class="edit-action-button p-1 rounded-md transition-all duration-150 ease-in-out"
title="Editar Empleado"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z" />
<path fill-rule="evenodd" d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" clip-rule="evenodd" />
</svg>
</button>
<button
@click="handleViewDetails(employee.id)"
class="text-green-600 hover:text-green-800 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 p-1 rounded-md hover:bg-green-100 transition-all duration-150 ease-in-out"
title="Ver Detalles del Empleado"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
</svg>
</button>
<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 hover:text-blue-800 hover:bg-blue-100 focus:ring-blue-500" 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="handleViewDetails(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-green-600 hover:text-green-800 hover:bg-green-100 focus:ring-green-500" title="Ver Detalles">
<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="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" /></svg>
</button>
</div>
</td>
</tr>
</tbody>
@@ -123,57 +112,17 @@ const handleViewDetails = (employeeId: string | number) => {
</script>
<style scoped>
/* Enhancing table aesthetics and interactivity */
.min-w-full {
border-collapse: separate; /* Allows for rounded corners on table if desired, or better spacing */
border-spacing: 0;
}
th {
font-weight: 600; /* semibold for headers */
color: #4b5563; /* gray-600 for header text */
}
td {
color: #374151; /* gray-700 for cell text */
}
/* Scoped styles can be minimized or removed if Tailwind covers all needs */
.rounded-full {
object-fit: cover; /* Ensures avatar images are displayed nicely within their circular bounds */
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); /* Subtle shadow for avatars */
}
/* Action button icon styling */
.edit-action-button {
color: var(--accent-color-empleados);
}
.edit-action-button:hover {
background-color: var(--accent-color-empleados);
color: white; /* Assuming accent color is dark enough for white text */
}
.edit-action-button:focus {
outline: none;
box-shadow: 0 0 0 2px var(--background-color), 0 0 0 4px var(--accent-color-empleados);
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.15); /* Slightly larger pop on hover for icons */
}
/* Ensure consistent padding and alignment */
.px-6 { padding-left: 1.5rem; padding-right: 1.5rem; }
.py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; }
.py-4 { padding-top: 1rem; padding-bottom: 1rem; }
/* Adding a subtle border to the table container itself */
.overflow-x-auto {
border: 1px solid #e5e7eb; /* Tailwind's gray-200 */
}
table.table-empleados {
background-color: var(--table-bg-color-empleados);
transform: scale(1.1); /* Adjusted scale for a subtler effect */
}
</style>

View File

@@ -1,33 +1,41 @@
<template>
<div class="tabla-planillas-container">
<table class="tabla-planillas">
<thead>
<div class="p-4 sm:p-6 bg-white shadow-md rounded-lg overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th>ID</th>
<th>Título</th>
<th>Empleado ID</th>
<th>Fecha Desde</th>
<th>Fecha Hasta</th>
<th>Total</th>
<th>Estado</th>
<th>Acciones</th>
<th class="px-4 py-3 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 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 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 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 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 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 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 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 uppercase tracking-wider">Acciones</th>
</tr>
</thead>
<tbody>
<tbody class="bg-white divide-y divide-gray-200">
<tr v-if="planillas && planillas.length === 0">
<td :colspan="8" style="text-align: center;">No hay planillas para mostrar.</td>
<td colspan="8" class="px-6 py-10 text-center text-gray-500 text-lg">No hay planillas para mostrar.</td>
</tr>
<tr v-for="planilla in planillas" :key="planilla.id">
<td>{{ planilla.id }}</td>
<td>{{ planilla.titulo }}</td>
<td>{{ planilla.empleado_id }}</td>
<td>{{ formatDate(planilla.fecha_desde) }}</td>
<td>{{ formatDate(planilla.fecha_hasta) }}</td>
<td>{{ formatCurrency(planilla.total) }}</td>
<td><span :class="`estado-${planilla.estado?.toLowerCase()}`">{{ planilla.estado }}</span></td>
<td>
<button @click="editPlanilla(planilla.id)" class="action-button edit-button">Editar</button>
<button @click="confirmDeletePlanilla(planilla)" class="action-button delete-button">Eliminar</button>
<tr v-for="planilla in planillas" :key="planilla.id" class="hover:bg-gray-100 transition-colors duration-150 ease-in-out">
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700">{{ planilla.id }}</td>
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700">{{ planilla.titulo }}</td>
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700">{{ planilla.empleado_id }}</td>
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700">{{ formatDate(planilla.fecha_desde) }}</td>
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700">{{ formatDate(planilla.fecha_hasta) }}</td>
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700">{{ formatCurrency(planilla.total) }}</td>
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700">
<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">
<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 hover:text-blue-800 hover:bg-blue-100 focus:ring-blue-500" 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 hover:text-red-800 hover:bg-red-100 focus:ring-red-500" 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>
@@ -38,6 +46,7 @@
<script setup>
import { defineProps, defineEmits } from 'vue';
import { usePlanillasStore } from '../../stores/usePlanillas';
import { formatDate, formatCurrency, getStatusClass } from '../../utils/formatters.js';
const props = defineProps({
planillas: {
@@ -51,24 +60,6 @@ const emit = defineEmits(['edit']); // Removed 'delete' as it's handled internal
const planillasStore = usePlanillasStore();
const formatDate = (dateString) => {
if (!dateString) return 'N/A';
const date = new Date(dateString);
return date.toLocaleDateString('es-ES', {
year: 'numeric',
month: '2-digit', // Changed to 2-digit for table compactness
day: '2-digit', // Changed to 2-digit for table compactness
});
};
const formatCurrency = (value) => {
if (value == null) return 'N/A';
return Number(value).toLocaleString('es-PY', {
style: 'currency',
currency: 'PYG',
});
};
const editPlanilla = (id) => {
emit('edit', id);
};
@@ -90,79 +81,3 @@ const deletePlanillaInternal = async (id) => {
}
};
</script>
<style scoped>
.tabla-planillas-container {
overflow-x: auto; /* Allows table to be scrolled horizontally if needed */
}
.tabla-planillas {
background-color: var(--table-bg-color-planillas); /* Added */
width: 100%;
border-collapse: collapse;
margin-top: 1em;
font-size: 0.9em;
}
.tabla-planillas th,
.tabla-planillas td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
.tabla-planillas th {
background-color: #f2f2f2;
font-weight: bold;
}
.tabla-planillas tr:nth-child(even) {
background-color: #f9f9f9;
}
.tabla-planillas tr:hover {
background-color: #f1f1f1;
}
.action-button {
padding: 5px 10px;
margin-right: 5px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9em;
}
.edit-button {
background-color: var(--accent-color-planillas);
color: white;
}
.edit-button:hover {
filter: brightness(0.9);
}
.delete-button {
background-color: var(--warning-color); /* Using warning color for delete */
color: white;
}
.delete-button:hover {
filter: brightness(0.9);
}
/* Status styling (similar to cardPlanilla) */
.estado-pagado {
color: green;
font-weight: bold;
}
.estado-pendiente {
color: orange;
font-weight: bold;
}
.estado-anulado {
color: red;
font-weight: bold;
/* text-decoration: line-through; */ /* Optional for table view */
}
</style>

View File

@@ -1,35 +1,43 @@
<template>
<div class="tabla-tareas-container">
<table class="tabla-tareas">
<thead>
<div class="p-4 sm:p-6 bg-white shadow-md rounded-lg overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th>ID</th>
<th>Título</th>
<th>Empleado ID</th>
<th>Fecha</th>
<th>Estado</th>
<th>Tipo</th>
<th>Precio</th>
<th>Planilla ID</th>
<th>Acciones</th>
<th class="px-4 py-3 sm:px-6 sm:py-3 text-left text-xs font-medium text-gray-500 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 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 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 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 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 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 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 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 uppercase tracking-wider">Acciones</th>
</tr>
</thead>
<tbody>
<tbody class="bg-white divide-y divide-gray-200">
<tr v-if="!tareas || tareas.length === 0">
<td :colspan="9" style="text-align: center;">No hay tareas para mostrar.</td>
<td colspan="9" class="px-6 py-10 text-center text-gray-500 text-lg">No hay tareas para mostrar.</td>
</tr>
<tr v-for="tarea in tareas" :key="tarea.id">
<td>{{ tarea.id }}</td>
<td>{{ tarea.titulo }}</td>
<td>{{ tarea.empleado_id }}</td>
<td>{{ formatDate(tarea.fecha) }}</td>
<td><span :class="`estado-${tarea.estado?.toLowerCase().replace(/\s+/g, '-')}`">{{ tarea.estado }}</span></td>
<td>{{ tarea.tipo || 'N/A' }}</td>
<td>{{ tarea.precio != null ? formatCurrency(tarea.precio) : 'N/A' }}</td>
<td>{{ tarea.planilla_id || 'N/A' }}</td>
<td>
<button @click="editTarea(tarea.id)" class="action-button edit-button">Editar</button>
<button @click="confirmDeleteTarea(tarea)" class="action-button delete-button">Eliminar</button>
<tr v-for="tarea in tareas" :key="tarea.id" class="hover:bg-gray-100 transition-colors duration-150 ease-in-out">
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700">{{ tarea.id }}</td>
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700">{{ tarea.titulo }}</td>
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700">{{ tarea.empleado_id }}</td>
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700">{{ formatDate(tarea.fecha) }}</td>
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700">
<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">{{ tarea.tipo || 'N/A' }}</td>
<td class="px-4 py-3 sm:px-6 sm:py-4 whitespace-nowrap text-sm text-gray-700">{{ 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">{{ 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">
<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 hover:text-blue-800 hover:bg-blue-100 focus:ring-blue-500" 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(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 hover:text-red-800 hover:bg-red-100 focus:ring-red-500" 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>
@@ -40,6 +48,7 @@
<script setup>
import { defineProps, defineEmits } from 'vue';
import { useTareasStore } from '../../stores/useTareas';
import { formatDate, formatCurrency, getStatusClass } from '../../utils/formatters.js';
const props = defineProps({
tareas: {
@@ -53,24 +62,6 @@ const emit = defineEmits(['edit']);
const tareasStore = useTareasStore();
const formatDate = (dateString) => {
if (!dateString) return 'N/A';
// Assuming dateString might be a full ISO string, ensure it's handled correctly by Date constructor
const date = new Date(dateString);
const day = String(date.getUTCDate()).padStart(2, '0'); // Use getUTCDate for consistency if dates are stored in UTC
const month = String(date.getUTCMonth() + 1).padStart(2, '0'); // Months are 0-indexed
const year = date.getUTCFullYear();
return `${day}/${month}/${year}`;
};
const formatCurrency = (value) => {
if (value == null) return 'N/A';
return Number(value).toLocaleString('es-PY', { // Paraguayan Guarani
style: 'currency',
currency: 'PYG',
});
};
const editTarea = (id) => {
emit('edit', id);
};
@@ -92,77 +83,3 @@ const deleteTareaInternal = async (id) => {
}
};
</script>
<style scoped>
.tabla-tareas-container {
overflow-x: auto;
}
.tabla-tareas {
background-color: var(--table-bg-color-tareas); /* Added */
width: 100%;
border-collapse: collapse;
margin-top: 1em;
font-size: 0.9em;
}
.tabla-tareas th,
.tabla-tareas td {
border: 1px solid #ddd;
padding: 10px;
text-align: left;
vertical-align: middle;
}
.tabla-tareas th {
background-color: #f4f6f8;
font-weight: 600;
color: #333;
}
.tabla-tareas tr:nth-child(even) {
background-color: #f9fafb;
}
.tabla-tareas tr:hover {
background-color: #f0f0f0;
}
.action-button {
padding: 6px 10px;
margin-right: 6px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.85em;
transition: background-color 0.2s ease, transform 0.1s ease;
}
.action-button:hover {
transform: translateY(-1px);
}
.edit-button {
background-color: var(--accent-color-tareas);
color: white;
}
.edit-button:hover {
filter: brightness(0.9); /* Standard hover effect */
}
.delete-button {
background-color: #dc3545; /* Red */
color: white;
}
.delete-button:hover {
background-color: #c82333;
}
/* Status styling (consistent with cardTarea) */
.estado-pendiente { color: #ff9800; font-weight: bold; } /* Orange */
.estado-realizada,
.estado-completada, /* Synonyms */
.estado-hecho { color: #4caf50; font-weight: bold; } /* Green */
.estado-en-progreso { color: #2196f3; font-weight: bold; } /* Blue */
.estado-anulada,
.estado-cancelada { color: #f44336; font-weight: bold; } /* Red */
</style>

View File

@@ -0,0 +1,96 @@
// ui/src/utils/formatters.js
export const formatDate = (dateString) => {
if (!dateString) return 'N/A';
const date = new Date(dateString);
// Using UTC methods for consistency if dates are stored in UTC
// For simple date, local might be fine, but UTC is safer for consistency across server/client
const day = String(date.getUTCDate()).padStart(2, '0');
const month = String(date.getUTCMonth() + 1).padStart(2, '0'); // Months are 0-indexed
const year = date.getUTCFullYear();
return `${day}/${month}/${year}`;
};
export const formatDateTime = (dateTimeString) => {
if (!dateTimeString) return 'N/A';
const date = new Date(dateTimeString);
const day = String(date.getUTCDate()).padStart(2, '0');
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
const year = date.getUTCFullYear();
const hours = String(date.getUTCHours()).padStart(2, '0');
const minutes = String(date.getUTCMinutes()).padStart(2, '0');
return `${day}/${month}/${year} ${hours}:${minutes}`;
};
export const formatCurrency = (value, currency = 'PYG') => {
if (value == null || isNaN(Number(value))) return 'N/A';
return Number(value).toLocaleString('es-PY', { // Default to Paraguayan Guarani
style: 'currency',
currency: currency,
minimumFractionDigits: 0, // Assuming whole numbers for PYG primarily
maximumFractionDigits: 0, // Adjust if decimals are common for the currency
});
};
export const truncateText = (text, maxLength = 50) => {
if (!text) return 'N/A';
if (text.length <= maxLength) return text;
return text.substring(0, maxLength) + '...';
};
// Generic status class generator
// It can be expanded with more specific module-based color themes if needed later
const defaultStatusStyles = {
base: 'px-2.5 py-0.5 rounded-full text-xs font-semibold',
yellow: 'bg-yellow-100 text-yellow-800',
green: 'bg-green-100 text-green-800',
red: 'bg-red-100 text-red-800',
blue: 'bg-blue-100 text-blue-800',
gray: 'bg-gray-100 text-gray-800',
};
// Status mappings for different modules/contexts can be centralized here
// For now, using a general mapping based on common terms observed
const commonStatusMap = {
// Asistencias
'pendiente': defaultStatusStyles.yellow,
'presente': defaultStatusStyles.green,
'confirmada': defaultStatusStyles.green,
'ausente': defaultStatusStyles.red,
'justificada': defaultStatusStyles.blue,
'cancelada': defaultStatusStyles.gray, // Could be red too, gray for less severe
'anulada': defaultStatusStyles.gray, // Same as cancelada
// Planillas
'pagado': defaultStatusStyles.green,
// 'pendiente' is already mapped
// 'anulado' is already mapped
// Tareas
// 'pendiente' is already mapped
'realizada': defaultStatusStyles.green,
'completada': defaultStatusStyles.green,
'hecho': defaultStatusStyles.green,
'en-progreso': defaultStatusStyles.blue,
// 'anulada' is already mapped
// 'cancelada' is already mapped
};
export const getStatusClass = (status) => {
const baseClasses = defaultStatusStyles.base;
if (!status) return `${baseClasses} ${defaultStatusStyles.gray}`;
const lowerStatus = String(status).toLowerCase().replace(/\s+/g, '-');
const style = commonStatusMap[lowerStatus] || defaultStatusStyles.gray;
return `${baseClasses} ${style}`;
};
// Example of how it could be more module specific if needed:
/*
export const getAsistenciaStatusClass = (status) => {
const baseClasses = defaultStatusStyles.base;
// ... Asistencia specific logic ...
return `${baseClasses} ${specificStyle}`;
}
*/