Merge pull request #15 from josedario87/feat/standardize-ui-tables
Feat/standardize UI tables
This commit is contained in:
49
ui/README.md
49
ui/README.md
@@ -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.
|
||||
|
||||
@@ -1,31 +1,39 @@
|
||||
<template>
|
||||
<div class="tabla-asistencias-container">
|
||||
<table class="tabla-asistencias">
|
||||
<thead>
|
||||
<div class="p-4 sm:p-6 rounded-lg overflow-x-auto" :style="{ backgroundColor: ui.tableBgColorAsistencias }">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50 dark:bg-slate-700">
|
||||
<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 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>
|
||||
<tbody class="bg-white dark:bg-slate-800 divide-y divide-gray-200 dark:divide-slate-700">
|
||||
<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 dark:text-slate-400 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="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>
|
||||
@@ -36,6 +44,10 @@
|
||||
<script setup>
|
||||
import { defineProps, defineEmits } from 'vue';
|
||||
import { useAsistenciasStore } from '../../stores/useAsistencias';
|
||||
import { formatDateTime, truncateText, getStatusClass } from '../../utils/formatters.js';
|
||||
import { useUi } from '../../stores/useUi.js';
|
||||
|
||||
const ui = useUi();
|
||||
|
||||
const props = defineProps({
|
||||
asistencias: {
|
||||
@@ -49,24 +61,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 +81,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>
|
||||
|
||||
@@ -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">
|
||||
<thead class="bg-gray-50">
|
||||
<div class="p-4 sm:p-6 rounded-lg overflow-x-auto" :style="{ backgroundColor: ui.tableBgColorEmpleados }">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50 dark:bg-slate-700">
|
||||
<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 dark:text-slate-400 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 dark:text-slate-400 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 dark:text-slate-400 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 dark:text-slate-400 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 dark:text-slate-400 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 dark:text-slate-400 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 dark:text-slate-400 uppercase tracking-wider">
|
||||
Acciones
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
<tbody class="bg-white dark:bg-slate-800 divide-y divide-gray-200 dark:divide-slate-700">
|
||||
<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 dark:text-slate-400 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">
|
||||
<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-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm font-semibold text-gray-900">{{ employee.name }}</div>
|
||||
<div v-if="employee.grupo_estudio" class="text-xs text-gray-500">
|
||||
<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-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 dark:text-slate-300">
|
||||
{{ 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 dark:text-slate-300">
|
||||
{{ 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 dark:text-slate-300">
|
||||
{{ 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 dark:text-slate-300">
|
||||
{{ 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 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="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 dark:text-green-400 hover:text-green-800 dark:hover:text-green-300 hover:bg-green-100 dark:hover:bg-green-600/20 focus:ring-green-500 dark:focus:ring-green-400" 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>
|
||||
@@ -88,6 +77,9 @@
|
||||
<script setup lang="ts">
|
||||
import { PropType } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useUi } from '../../stores/useUi.js';
|
||||
|
||||
const ui = useUi();
|
||||
|
||||
// Interface for Employee object structure, aligning with prisma model (excluding sensitive or large fields for table view)
|
||||
interface Employee {
|
||||
@@ -123,57 +115,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>
|
||||
|
||||
@@ -1,33 +1,41 @@
|
||||
<template>
|
||||
<div class="tabla-planillas-container">
|
||||
<table class="tabla-planillas">
|
||||
<thead>
|
||||
<div class="p-4 sm:p-6 rounded-lg overflow-x-auto" :style="{ backgroundColor: ui.tableBgColorPlanillas }">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50 dark:bg-slate-700">
|
||||
<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 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>
|
||||
<tbody class="bg-white dark:bg-slate-800 divide-y divide-gray-200 dark:divide-slate-700">
|
||||
<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 dark:text-slate-400 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="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>
|
||||
@@ -38,6 +46,10 @@
|
||||
<script setup>
|
||||
import { defineProps, defineEmits } from 'vue';
|
||||
import { usePlanillasStore } from '../../stores/usePlanillas';
|
||||
import { formatDate, formatCurrency, getStatusClass } from '../../utils/formatters.js';
|
||||
import { useUi } from '../../stores/useUi.js';
|
||||
|
||||
const ui = useUi();
|
||||
|
||||
const props = defineProps({
|
||||
planillas: {
|
||||
@@ -51,24 +63,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 +84,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>
|
||||
|
||||
@@ -1,35 +1,74 @@
|
||||
<template>
|
||||
<div class="tabla-tareas-container">
|
||||
<table class="tabla-tareas">
|
||||
<thead>
|
||||
<div
|
||||
class="p-4 sm:p-6 rounded-lg overflow-x-auto"
|
||||
:style="{ '--bg-tareas': ui.tableBgColorTareas }"
|
||||
>
|
||||
<table
|
||||
class="min-w-full divide-y divide-[var(--accent-color-tareas)]"
|
||||
:style="{ backgroundColor: ui.tableBgColorTareas }"
|
||||
>
|
||||
<thead
|
||||
class="divide-y divide-gray-200 dark:divide-slate-700"
|
||||
:style="{ backgroundColor: ui.tableBgColorTareas }"
|
||||
>
|
||||
<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 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>
|
||||
|
||||
<tbody
|
||||
class="divide-y divide-[var(--accent-color-tareas)]"
|
||||
:style="{ backgroundColor: ui.tableBgColorTareas }"
|
||||
>
|
||||
<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 dark:text-slate-400 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="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"
|
||||
>
|
||||
<!-- ícono editar -->
|
||||
</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"
|
||||
>
|
||||
<!-- ícono eliminar -->
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -38,131 +77,33 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps, defineEmits } from 'vue';
|
||||
import { useTareasStore } from '../../stores/useTareas';
|
||||
import { defineProps, defineEmits } from 'vue'
|
||||
import { useTareasStore } from '@/stores/useTareas'
|
||||
import { formatDate, formatCurrency, getStatusClass } from '@/utils/formatters'
|
||||
import { useUi } from '@/stores/useUi'
|
||||
|
||||
const props = defineProps({
|
||||
tareas: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
const ui = useUi()
|
||||
|
||||
const emit = defineEmits(['edit']);
|
||||
defineProps({
|
||||
tareas: { type: Array, required: true, default: () => [] },
|
||||
})
|
||||
|
||||
const tareasStore = useTareasStore();
|
||||
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);
|
||||
};
|
||||
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);
|
||||
// Optional: Show success notification or emit 'deleted' event
|
||||
} catch (error) {
|
||||
console.error(`Error deleting tarea with id ${id}:`, error);
|
||||
alert('Ocurrió un error al eliminar la tarea.');
|
||||
// Optional: Show error notification
|
||||
await tareasStore.deleteTarea(id)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
alert('Ocurrió un error al eliminar la tarea.')
|
||||
}
|
||||
};
|
||||
}
|
||||
</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>
|
||||
|
||||
@@ -18,6 +18,12 @@
|
||||
--accent-color-tareas: #4CAF50;
|
||||
--accent-color-planillas: #FF9800;
|
||||
--accent-color-asistencias: #E91E63;
|
||||
|
||||
/* NEW: Module-specific table container background colors (Light Theme) */
|
||||
--table-container-bg-color-asistencias: #fdebee;
|
||||
--table-container-bg-color-empleados: #e3f2fd;
|
||||
--table-container-bg-color-planillas: #fff8e1;
|
||||
--table-container-bg-color-tareas: #e6f4ea;
|
||||
}
|
||||
|
||||
html.theme-dark {
|
||||
@@ -26,6 +32,12 @@ html.theme-dark {
|
||||
--warning-color: #FFA000; /* Example dark theme warning */
|
||||
--background-color: #303030; /* Dark theme background */
|
||||
--text-color: #FFFFFF; /* Text color for dark theme */
|
||||
|
||||
/* NEW: Module-specific table container background colors (Dark Theme) */
|
||||
--table-container-bg-color-asistencias: #4a1f28;
|
||||
--table-container-bg-color-empleados: #193a50;
|
||||
--table-container-bg-color-planillas: #503f1b;
|
||||
--table-container-bg-color-tareas: #1e4028;
|
||||
}
|
||||
|
||||
/* Apply background and text color to the body for theme changes */
|
||||
|
||||
96
ui/src/utils/formatters.js
Normal file
96
ui/src/utils/formatters.js
Normal 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}`;
|
||||
}
|
||||
*/
|
||||
Reference in New Issue
Block a user