Merge remote-tracking branch 'github/main'
This commit is contained in:
102
ui/README.md
102
ui/README.md
@@ -1,5 +1,105 @@
|
||||
# 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).
|
||||
|
||||
|
||||
## Standardized Card Components
|
||||
|
||||
This section documents the standardized card components used throughout the UI for modules like Asistencias, Empleados, Planillas, and Tareas. These components have been refactored for a consistent structure, styling approach, and user experience.
|
||||
|
||||
### Overview
|
||||
|
||||
Card components provide a summarized display of an item (e.g., an employee, a task) and offer quick actions. The standardization ensures that users encounter a familiar layout and interaction pattern across different modules.
|
||||
|
||||
### Common HTML Structure
|
||||
|
||||
All standardized card components share a common HTML structure:
|
||||
|
||||
- **Root Element:** `<div class="card ...">`
|
||||
- This is the main container for the card.
|
||||
- Styling is primarily applied using Tailwind CSS utility classes. Common classes include `bg-white 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`.
|
||||
- **Card Header:** `<div class="card-header ...">`
|
||||
- Typically contains the title or ID of the item and a status badge.
|
||||
- Styled with Tailwind: `flex justify-between items-center mb-3 md:mb-4 pb-2 md:pb-3 border-b border-gray-100`.
|
||||
- The title element (e.g., `<h4>`) uses module-specific accent colors.
|
||||
- **Card Body:** `<div class="card-body ...">`
|
||||
- Displays the main content fields of the item.
|
||||
- Styled with Tailwind: `text-sm text-gray-700 space-y-2`.
|
||||
- **Card Actions:** `<div class="card-actions ...">`
|
||||
- Contains action buttons like "Editar" and "Eliminar".
|
||||
- Styled with Tailwind: `mt-auto pt-3 md:pt-4 flex justify-end space-x-2 md:space-x-3`.
|
||||
|
||||
### Styling with Tailwind CSS
|
||||
|
||||
The visual appearance of the cards (layout, spacing, typography, borders, shadows) is managed by [Tailwind CSS](https://tailwindcss.com/).
|
||||
- Utility classes are applied directly in the component templates (`<template>` section of `.vue` files).
|
||||
- The Tailwind configuration can be found in `ui/tailwind.config.js`.
|
||||
- Core Tailwind directives (`@tailwind base; @tailwind components; @tailwind utilities;`) are included in `ui/src/style.css`.
|
||||
|
||||
### Theming with CSS Variables
|
||||
|
||||
While Tailwind handles most styling, module-specific theming (especially accent colors) and some global style properties are controlled by CSS variables. These variables are defined in the `:root` scope in `ui/src/style.css`.
|
||||
|
||||
**Key CSS Variables:**
|
||||
|
||||
- **Module Accent Colors:**
|
||||
- `--accent-color-asistencias`: Accent color for the Asistencias module (e.g., for headers, buttons).
|
||||
- `--accent-color-empleados`: Accent color for the Empleados module.
|
||||
- `--accent-color-planillas`: Accent color for the Planillas module.
|
||||
- `--accent-color-tareas`: Accent color for the Tareas module.
|
||||
- **Common Colors & Styles:**
|
||||
- `--warning-color`: Used for delete buttons and other warning indicators (e.g., `#dc3545`).
|
||||
- `--background-color`: Default background for the application.
|
||||
- `--text-color`: Default text color.
|
||||
- `--muted-text-color`: For less prominent text.
|
||||
- `--border-color`: Default border color.
|
||||
- `--card-shadow`: Default shadow for cards.
|
||||
- `--card-hover-shadow`: Shadow for cards on hover.
|
||||
|
||||
**Usage Example (in a Vue component):**
|
||||
|
||||
```html
|
||||
<h4 :style="{ color: 'var(--accent-color-empleados)' }">Employee Name</h4>
|
||||
|
||||
<button :style="{ backgroundColor: 'var(--accent-color-empleados)' }">Edit Employee</button>
|
||||
```
|
||||
Alternatively, Tailwind's arbitrary value support can be used with CSS variables:
|
||||
```html
|
||||
<h4 class="text-[var(--accent-color-empleados)]">Employee Name</h4>
|
||||
<button class="bg-[var(--accent-color-empleados)] hover:bg-[var(--accent-color-empleados)]/90 ...">Edit</button>
|
||||
```
|
||||
The latter approach (Tailwind arbitrary values) is generally preferred for consistency with other Tailwind classes, but direct `:style` binding is also used, especially for properties not easily covered by Tailwind utilities or for dynamic hover effects managed in JavaScript.
|
||||
|
||||
### Functionality
|
||||
|
||||
- **Actions:**
|
||||
- **Edit:** Navigates to the form view for editing the item (e.g., `/empleados/:id`).
|
||||
- **Delete:** Initiates a delete process for the item.
|
||||
- **Delete Confirmation:** A standardized JavaScript `confirm()` dialog is used before deleting an item, with a message format like: `¿Está seguro de que desea eliminar [tipo de item] "[nombre/ID del item]" (ID: [ID])?`.
|
||||
|
||||
### Component Usage
|
||||
|
||||
Each card component is designed to be used within its respective module's views. They typically expect a prop containing the item data and emit an `edit` event.
|
||||
|
||||
**Example (`cardAsistencia.vue`):**
|
||||
```vue
|
||||
<template>
|
||||
<cardAsistencia :asistencia="asistenciaData" @edit="handleEditAsistencia" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// ...
|
||||
const asistenciaData = { id: 1, /* ... other properties */ };
|
||||
const handleEditAsistencia = (asistenciaId) => {
|
||||
// Navigate to edit page or open modal
|
||||
};
|
||||
</script>
|
||||
```
|
||||
The specific prop name matches the module (e.g., `asistencia` for `cardAsistencia`, `employee` for `cardEmpleado`, `planilla` for `cardPlanilla`, `tarea` for `cardTarea`).
|
||||
|
||||
|
||||
6
ui/postcss.config.js
Normal file
6
ui/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
// Tailwind CSS is handled by the @tailwindcss/vite plugin in vite.config.js
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { watchEffect } from 'vue'
|
||||
import { watchEffect, computed } from 'vue' // Added computed
|
||||
import TopBar from '@/components/ui/TopBar.vue'
|
||||
import NavBar from '@/components/ui/NavBar.vue'
|
||||
import { useUi } from '@/stores/useUi'
|
||||
@@ -46,6 +46,15 @@ watchEffect(() => {
|
||||
root.classList.add('animations-disabled')
|
||||
}
|
||||
})
|
||||
|
||||
const transitionDurationStyle = computed(() => {
|
||||
// Assuming base duration of 0.3s for normal speed (transitionSpeed = 1)
|
||||
const baseDuration = 0.3
|
||||
const effectiveDuration = baseDuration * ui.transitionSpeed
|
||||
return {
|
||||
'--current-transition-duration': `${effectiveDuration}s`
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -59,13 +68,17 @@ watchEffect(() => {
|
||||
// The global style.css will handle base background and text color via body styling
|
||||
// but we can keep specific overrides here if needed or theme classes.
|
||||
// ui.theme === 'dark' ? 'bg-gray-800 text-gray-100' : 'bg-gray-100 text-gray-900'
|
||||
]">
|
||||
]" :style="transitionDurationStyle">
|
||||
<!-- NavBar fija -->
|
||||
<NavBar />
|
||||
|
||||
<!-- contenido principal -->
|
||||
<main class="min-h-[calc(100vh-56px)] flex flex-col overflow-hidden">
|
||||
<RouterView class="flex-1 overflow-auto" />
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="slide-fade" mode="out-in">
|
||||
<component :is="Component" class="flex-1 overflow-auto" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
@@ -73,4 +86,26 @@ watchEffect(() => {
|
||||
<style scoped>
|
||||
/* Scoped styles remain, global styles are in style.css */
|
||||
/* We can add specific App.vue styling here if needed, that doesn't rely on theme variables directly */
|
||||
|
||||
.slide-fade-enter-active,
|
||||
.slide-fade-leave-active {
|
||||
transition: all var(--current-transition-duration) ease-out; /* Use the CSS variable */
|
||||
}
|
||||
|
||||
.slide-fade-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(-20px); /* Slide in from the left */
|
||||
}
|
||||
|
||||
.slide-fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(20px); /* Slide out to the right */
|
||||
}
|
||||
|
||||
/* Ensure content is fully opaque and in place when active/not transitioning */
|
||||
.slide-fade-enter-to,
|
||||
.slide-fade-leave-from {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,24 +1,32 @@
|
||||
<template>
|
||||
<div class="asistencia-card">
|
||||
<div class="card-header">
|
||||
<h4>Asistencia ID: {{ asistencia.id }}</h4>
|
||||
<span :class="`estado-asistencia estado-${asistencia.estado?.toLowerCase().replace(/\s+/g, '-')}`">
|
||||
<div :style="{ backgroundColor: ui.tableBgColorAsistencias }" class="shadow-md rounded-lg p-4 md:p-6 m-2 border border-gray-200 hover:shadow-lg transition-shadow duration-300 ease-in-out flex flex-col">
|
||||
<div class="flex justify-between items-center mb-3 md:mb-4 pb-2 md:pb-3 border-b border-gray-100">
|
||||
<h4 class="text-lg md:text-xl font-semibold" :style="{ color: 'var(--accent-color-asistencias)' }">Asistencia ID: {{ asistencia.id }}</h4>
|
||||
<span :class="['px-2 py-1 text-xs font-bold text-white rounded-full', getStatusClass(asistencia.estado)]">
|
||||
{{ asistencia.estado || 'N/A' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p><strong>Empleado ID:</strong> {{ asistencia.empleado_id }}</p>
|
||||
<p><strong>Entrada:</strong> {{ formatDateTime(asistencia.entrada) }}</p>
|
||||
<p><strong>Salida:</strong> {{ asistencia.salida ? formatDateTime(asistencia.salida) : 'No registrada' }}</p>
|
||||
<p v-if="asistencia.observacion" class="observacion">
|
||||
<strong>Observación:</strong> {{ asistencia.observacion }}
|
||||
<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>
|
||||
<!-- Historial might be too complex for a card, but can be added if needed -->
|
||||
<!-- <p v-if="asistencia.historial"><strong>Historial:</strong> {{ asistencia.historial }}</p> -->
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<button @click="editAsistencia" class="action-button edit-button">Editar</button>
|
||||
<button @click="confirmDeleteAsistencia" class="action-button delete-button">Eliminar</button>
|
||||
<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>
|
||||
@@ -26,6 +34,10 @@
|
||||
<script setup>
|
||||
import { defineProps, defineEmits } from 'vue';
|
||||
import { useAsistenciasStore } from '../../stores/useAsistencias';
|
||||
import { useUi } from '../../stores/useUi.js';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const ui = useUi();
|
||||
|
||||
const props = defineProps({
|
||||
asistencia: {
|
||||
@@ -41,14 +53,7 @@ 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' }); // Spanish locale
|
||||
// year: 'numeric', // Already present in the new options
|
||||
// month: '2-digit', // Using 2-digit for month for brevity in card // Already present in the new options
|
||||
// day: '2-digit', // Using 2-digit for day // Already present in the new options
|
||||
// hour: '2-digit', // Already present in the new options
|
||||
// minute: '2-digit', // Already present in the new options
|
||||
// second: '2-digit', // Optional: include seconds
|
||||
// }); // Removed this line as options are now in a single line
|
||||
return date.toLocaleString('es-HN', { timeZone: 'America/Tegucigalpa', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
|
||||
};
|
||||
|
||||
const editAsistencia = () => {
|
||||
@@ -56,7 +61,7 @@ const editAsistencia = () => {
|
||||
};
|
||||
|
||||
const confirmDeleteAsistencia = () => {
|
||||
if (confirm(`¿Está seguro de que desea eliminar esta asistencia (ID: ${props.asistencia.id})?`)) {
|
||||
if (confirm(`¿Está seguro de que desea eliminar la asistencia "${props.asistencia.id}" (ID: ${props.asistencia.id})?`)) {
|
||||
deleteAsistenciaInternal();
|
||||
}
|
||||
};
|
||||
@@ -64,126 +69,48 @@ const confirmDeleteAsistencia = () => {
|
||||
const deleteAsistenciaInternal = async () => {
|
||||
try {
|
||||
await asistenciasStore.deleteAsistencia(props.asistencia.id);
|
||||
// Optionally, emit 'deleted' or show notification
|
||||
} catch (error) {
|
||||
console.error('Error deleting asistencia:', error);
|
||||
alert('Ocurrió un error al eliminar la asistencia.');
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusClass = (status) => {
|
||||
if (!status) return 'bg-gray-400'; // Default for N/A
|
||||
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 '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%)';
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.asistencia-card {
|
||||
border: 1px solid #e0e0e0; /* Lighter border for a softer look */
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 8px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.05); /* Softer shadow */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: box-shadow 0.3s ease-in-out;
|
||||
}
|
||||
.asistencia-card:hover {
|
||||
box-shadow: 0 4px 10px rgba(0,0,0,0.1); /* Enhanced shadow on hover */
|
||||
}
|
||||
/* 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 */
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 10px; /* Increased padding */
|
||||
border-bottom: 1px solid #f0f0f0; /* Even lighter border */
|
||||
/* 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.
|
||||
*/
|
||||
.border-l-3 {
|
||||
border-left-width: 3px;
|
||||
}
|
||||
|
||||
.card-header h4 { /* Style for the new Asistencia ID header */
|
||||
font-weight: bold;
|
||||
color: var(--accent-color-asistencias); /* Accent color */
|
||||
font-size: 1.15em; /* Slightly larger for main header element */
|
||||
margin: 0; /* Remove default margin */
|
||||
}
|
||||
|
||||
.empleado-id { /* This class is no longer used in the header, but could be reused elsewhere if needed */
|
||||
font-weight: bold;
|
||||
color: var(--accent-color-asistencias); /* Accent color */
|
||||
font-size: 1.05em;
|
||||
}
|
||||
|
||||
.estado-asistencia {
|
||||
padding: 5px 10px; /* Slightly more padding */
|
||||
border-radius: 15px; /* Pill shape */
|
||||
font-size: 0.8em;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
text-transform: uppercase; /* Uppercase status */
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.card-body p {
|
||||
margin: 8px 0;
|
||||
color: #555;
|
||||
font-size: 0.95em;
|
||||
line-height: 1.6; /* Improved line spacing */
|
||||
}
|
||||
.card-body p strong {
|
||||
color: #333;
|
||||
}
|
||||
.observacion {
|
||||
font-style: italic;
|
||||
color: #666;
|
||||
background-color: #f8f9fa; /* Very light grey background */
|
||||
padding: 10px; /* More padding */
|
||||
border-left: 3px solid var(--accent-color-asistencias); /* Accent color for border */
|
||||
border-radius: 4px;
|
||||
margin-top:10px;
|
||||
font-size: 0.9em; /* Slightly smaller for observation */
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
margin-top: auto;
|
||||
padding-top: 16px; /* More space before actions */
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
padding: 8px 15px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
font-weight: 500; /* Slightly bolder text on buttons */
|
||||
transition: background-color 0.2s ease, transform 0.1s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
.action-button:hover{
|
||||
transform: translateY(-2px); /* More pronounced lift */
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.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 */
|
||||
.estado-pendiente { background-color: #ffc107; color: #212529;} /* Amber, dark text */
|
||||
.estado-presente,
|
||||
.estado-confirmada { background-color: #28a745; color: white;} /* Green */
|
||||
.estado-ausente { background-color: #dc3545; color: white;} /* Red */
|
||||
.estado-justificada { background-color: #17a2b8; color: white;} /* Info Blue */
|
||||
.estado-cancelada,
|
||||
.estado-anulada { background-color: #6c757d; color: white;} /* Gray */
|
||||
</style>
|
||||
|
||||
@@ -1,31 +1,52 @@
|
||||
<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-[var(--accent-color-asistencias)]"
|
||||
:style="{ backgroundColor: ui.tableBgColorAsistencias }"
|
||||
>
|
||||
<thead
|
||||
class="divide-y divide-[var(--accent-color-asistencias)]"
|
||||
:style="{ backgroundColor: ui.tableBgColorAsistencias }"
|
||||
>
|
||||
<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="divide-y divide-[var(--accent-color-asistencias)]"
|
||||
:style="{ backgroundColor: ui.tableBgColorAsistencias }"
|
||||
>
|
||||
<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 +57,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 +74,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 +94,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,11 +1,24 @@
|
||||
<script setup>
|
||||
import { ref, nextTick, onMounted, watch } from 'vue'
|
||||
import { ref, nextTick, onMounted, watch, computed } from 'vue'
|
||||
import { useChat } from '@/stores/useChat'
|
||||
import { useUi } from '@/stores/useUi'
|
||||
|
||||
const chat = useChat()
|
||||
const ui = useUi()
|
||||
const msg = ref('')
|
||||
const list = ref(null)
|
||||
|
||||
const chatStyleVariables = computed(() => ({
|
||||
'--chat-agent-message-color': ui.chatAgentMessageColor,
|
||||
'--chat-own-message-color': ui.chatOwnMessageColor,
|
||||
'--chat-input-box-color': ui.chatInputBoxColor,
|
||||
'--chat-accent-color': ui.chatAccentColor,
|
||||
'--chat-background-color': ui.chatBackgroundColor,
|
||||
'--chat-font-color': ui.chatFontColor,
|
||||
'--chat-font-family': ui.chatFontFamily || 'inherit', // Fallback to inherit if empty
|
||||
'--chat-font-size': ui.chatFontSize ? `${ui.chatFontSize}px` : 'inherit', // Fallback to inherit
|
||||
}))
|
||||
|
||||
function scrollBottom () {
|
||||
nextTick(() => list.value?.scrollTo({ top: list.value.scrollHeight, behavior: 'smooth' }))
|
||||
}
|
||||
@@ -40,7 +53,7 @@ watch(() => chat.items.length, scrollBottom)
|
||||
|
||||
<template>
|
||||
<!-- se adapta al contenedor flex, sin superponer la sidebar -->
|
||||
<div class="flex flex-col flex-1 min-h-0 bg-gray-50">
|
||||
<div class="flex flex-col flex-1 min-h-0" :style="chatStyleVariables" style="background-color: var(--chat-background-color);">
|
||||
<!-- historial -->
|
||||
<div ref="list" class="flex-1 min-h-0 overflow-auto p-6 space-y-4 custom-scroll">
|
||||
<template v-for="(m,i) in chat.items" :key="i">
|
||||
@@ -48,7 +61,14 @@ watch(() => chat.items.length, scrollBottom)
|
||||
<div :class="m.owner==='yo' ? 'flex justify-end' : 'flex justify-start'" v-if="m.type==='text'">
|
||||
<div
|
||||
class="max-w-lg rounded-lg px-4 py-2 shadow break-words"
|
||||
:class="m.owner==='yo' ? 'bg-teal-600 text-white' : 'bg-white text-gray-900'">
|
||||
:class="m.owner === 'yo' ? '' : ''"
|
||||
:style="{
|
||||
backgroundColor: m.owner === 'yo' ? 'var(--chat-own-message-color)' : 'var(--chat-agent-message-color)',
|
||||
color: 'var(--chat-font-color)',
|
||||
fontFamily: 'var(--chat-font-family)',
|
||||
fontSize: 'var(--chat-font-size)'
|
||||
}">
|
||||
|
||||
{{ m.text }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -59,15 +79,24 @@ watch(() => chat.items.length, scrollBottom)
|
||||
</div>
|
||||
|
||||
<!-- input -->
|
||||
<form @submit.prevent="send" class="border-t bg-white p-4 flex gap-2">
|
||||
<form @submit.prevent="send" class="border-t p-4 flex gap-2" :style="{ backgroundColor: 'var(--chat-input-box-color)' }">
|
||||
<textarea
|
||||
v-model="msg"
|
||||
@keydown="handleKey"
|
||||
rows="1"
|
||||
placeholder="Escribí un mensaje… (Enter para enviar, Shift+Enter salto)"
|
||||
class="flex-1 resize-none rounded-lg border p-3 focus:outline-none focus:ring-2 focus:ring-teal-500 custom-scroll"
|
||||
|
||||
class="flex-1 resize-none rounded-lg border p-3 focus:outline-none focus:ring-2 custom-scroll"
|
||||
:style="{
|
||||
backgroundColor: 'var(--chat-input-box-color)',
|
||||
borderColor: 'var(--chat-accent-color)',
|
||||
'--tw-ring-color': 'var(--chat-accent-color)',
|
||||
color: 'var(--chat-font-color)',
|
||||
fontFamily: 'var(--chat-font-family)',
|
||||
fontSize: 'var(--chat-font-size)'
|
||||
}"
|
||||
/>
|
||||
<button type="submit" class="px-4 py-2 rounded-lg bg-teal-600 text-white hover:bg-teal-700 transition">
|
||||
<button type="submit" class="px-4 py-2 rounded-lg text-white transition" :style="{ backgroundColor: 'var(--chat-accent-color)' }">
|
||||
➤
|
||||
</button>
|
||||
</form>
|
||||
@@ -75,9 +104,27 @@ watch(() => chat.items.length, scrollBottom)
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.canvas-chat-root {
|
||||
background-color: var(--background-color-chat, #F0F0F0); /* Fallback to store default */
|
||||
}
|
||||
|
||||
/* Default accent color for chat if not provided by CSS variable */
|
||||
:root {
|
||||
--accent-color-chat-fallback: #0D9488; /* Teal-700 as a fallback */
|
||||
--accent-color-chat-alpha-35-fallback: rgba(13, 148, 136, 0.35);
|
||||
--accent-color-chat-alpha-70-fallback: rgba(13, 148, 136, 0.7);
|
||||
--accent-color-chat-alpha-60-fallback: rgba(13, 148, 136, 0.6);
|
||||
}
|
||||
|
||||
.custom-scroll::-webkit-scrollbar { width: 8px; }
|
||||
.custom-scroll::-webkit-scrollbar-track { background: transparent; }
|
||||
.custom-scroll::-webkit-scrollbar-thumb { background-color: rgba(13,148,136,.35); border-radius: 4px; }
|
||||
.custom-scroll:hover::-webkit-scrollbar-thumb { background-color: rgba(13,148,136,.7); }
|
||||
.custom-scroll { scrollbar-width: thin; scrollbar-color: rgba(13,148,136,.6) transparent; }
|
||||
.custom-scroll::-webkit-scrollbar-track { background: transparent; } /* Consider if track needs to adapt to --chat-background-color */
|
||||
.custom-scroll::-webkit-scrollbar-thumb { background-color: var(--chat-accent-color); opacity: 0.35; border-radius: 4px; }
|
||||
.custom-scroll:hover::-webkit-scrollbar-thumb { opacity: 0.7; }
|
||||
/* For Firefox scrollbar, if needed, though limited styling capability compared to webkit */
|
||||
.custom-scroll { scrollbar-width: thin; scrollbar-color: var(--chat-accent-color) transparent; }
|
||||
|
||||
/* Override Tailwind focus ring color if it doesn't pick up the CSS variable directly */
|
||||
textarea:focus {
|
||||
--tw-ring-color: var(--chat-accent-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,132 +1,106 @@
|
||||
<template>
|
||||
<div class="bg-white shadow-lg rounded-lg p-6 m-4 w-full max-w-sm hover:shadow-xl transition-shadow duration-300 ease-in-out">
|
||||
<div class="flex items-center mb-4">
|
||||
<div :style="{ backgroundColor: ui.tableBgColorEmpleados }" class="shadow-md rounded-lg p-4 md:p-6 m-2 border border-gray-200 hover:shadow-lg transition-shadow duration-300 ease-in-out flex flex-col">
|
||||
<div class="flex items-center mb-3 md:mb-4 pb-2 md:pb-3 border-b border-gray-100">
|
||||
<img
|
||||
:src="employee.avatar_url || 'https://via.placeholder.com/150'"
|
||||
alt="Avatar del empleado"
|
||||
class="w-20 h-20 rounded-full mr-6 border-2 object-cover employee-avatar-border"
|
||||
class="w-16 h-16 rounded-full mr-4 border-2 object-cover"
|
||||
:style="{ borderColor: 'var(--accent-color-empleados)' }"
|
||||
/>
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-gray-800">{{ employee.name }}</h2>
|
||||
<p class="text-gray-600 text-sm">ID: {{ employee.id }}</p>
|
||||
<h2 class="text-lg md:text-xl font-semibold" :style="{ color: 'var(--accent-color-empleados)' }">{{ employee.name }}</h2>
|
||||
<p class="text-xs text-gray-500">ID: {{ employee.id }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div v-if="employee.telefono" class="flex items-center text-gray-700">
|
||||
<svg class="w-5 h-5 mr-2 employee-icon" 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>
|
||||
<span>{{ employee.telefono }}</span>
|
||||
</div>
|
||||
<div v-if="employee.ubicacion" class="flex items-center text-gray-700">
|
||||
<svg class="w-5 h-5 mr-2 employee-icon" 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>
|
||||
<span>{{ employee.ubicacion }}</span>
|
||||
</div>
|
||||
<div v-if="employee.cedula" class="flex items-center text-gray-700">
|
||||
<svg class="w-5 h-5 mr-2 employee-icon" 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>
|
||||
<span>Cedula: {{ employee.cedula }}</span>
|
||||
</div>
|
||||
<div v-if="employee.grupo_estudio" class="flex items-center text-gray-700">
|
||||
<svg class="w-5 h-5 mr-2 employee-icon" 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>
|
||||
<span>Grupo Estudio: {{ employee.grupo_estudio }}</span>
|
||||
</div>
|
||||
<div class="text-sm text-gray-700 space-y-2">
|
||||
<p v-if="employee.telefono" class="flex items-center">
|
||||
<svg class="w-5 h-5 mr-2 shrink-0" :style="{ color: 'var(--accent-color-empleados)' }" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.308 1.154a11.042 11.042 0 005.516 5.516l1.154-2.308a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"></path></svg>
|
||||
<strong class="font-medium text-gray-900">Teléfono:</strong> <span>{{ employee.telefono }}</span>
|
||||
</p>
|
||||
<p v-if="employee.ubicacion" class="flex items-center">
|
||||
<svg class="w-5 h-5 mr-2 shrink-0" :style="{ color: 'var(--accent-color-empleados)' }" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
|
||||
<strong class="font-medium text-gray-900">Ubicación:</strong> <span>{{ employee.ubicacion }}</span>
|
||||
</p>
|
||||
<p v-if="employee.cedula" class="flex items-center">
|
||||
<svg class="w-5 h-5 mr-2 shrink-0" :style="{ color: 'var(--accent-color-empleados)' }" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V8a2 2 0 00-2-2h-5m-4 0V5a2 2 0 012-2h2a2 2 0 012 2v1m-4 0h4m-6 10v-5m0 5v0z"></path></svg>
|
||||
<strong class="font-medium text-gray-900">Cédula:</strong> <span>{{ employee.cedula }}</span>
|
||||
</p>
|
||||
<p v-if="employee.grupo_estudio" class="flex items-center">
|
||||
<svg class="w-5 h-5 mr-2 shrink-0" :style="{ color: 'var(--accent-color-empleados)' }" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 14l9-5-9-5-9 5 9 5z"></path><path d="M12 14l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-5.998 12.083 12.083 0 01.665-6.479L12 14z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 14l9-5-9-5-9 5 9 5zm0 0v3.945m0-3.945L6.161 10.58M17.839 10.58L12 14m5.839-3.42L12 14m0 0l6.161 3.42m-6.161-3.42L5.839 14.002"></path></svg>
|
||||
<strong class="font-medium text-gray-900">Grupo Estudio:</strong> <span>{{ employee.grupo_estudio }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-6 flex justify-end space-x-3">
|
||||
<button @click="handleEdit" class="edit-button px-4 py-2 text-white text-sm font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2">
|
||||
Editar
|
||||
</button>
|
||||
<button @click="handleViewDetails" class="px-4 py-2 bg-gray-200 text-gray-700 text-sm font-medium rounded-md hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2">
|
||||
Ver Detalles
|
||||
</button>
|
||||
<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>
|
||||
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PropType } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useUi } from '../../stores/useUi.js';
|
||||
|
||||
// Define the structure of the employee object based on the Prisma schema
|
||||
interface Employee {
|
||||
id: string | number // Changed from BigInt to string | number for easier handling in frontend
|
||||
name: string
|
||||
cedula: number // Changed from BigInt
|
||||
avatar_url?: string
|
||||
telefono?: string
|
||||
ubicacion: string
|
||||
idciat?: string
|
||||
grupo_estudio?: string
|
||||
// created_at and updated_at are usually not displayed directly in a summary card
|
||||
// empleado: boolean // This is implicit as it's an employee card
|
||||
}
|
||||
const ui = useUi();
|
||||
const emit = defineEmits(['edit']);
|
||||
const empleadosStore = useEmpleadosStore();
|
||||
|
||||
const props = defineProps({
|
||||
employee: {
|
||||
type: Object as PropType<Employee>,
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
});
|
||||
|
||||
const handleEdit = () => {
|
||||
// Ensure employee.id is available and correctly typed for URL
|
||||
router.push(`/empleados/edit/${props.employee.id}`)
|
||||
}
|
||||
emit('edit', props.employee.id);
|
||||
};
|
||||
|
||||
const handleViewDetails = () => {
|
||||
// This could navigate to a more detailed employee page if one exists
|
||||
// For now, it can also navigate to an edit page or a specific detail view
|
||||
// Depending on the application's routing structure, this might be the same as edit or a different view
|
||||
router.push(`/empleados/view/${props.employee.id}`) // Assuming a dedicated view route exists or will be created
|
||||
}
|
||||
const confirmDeleteEmpleado = () => {
|
||||
if (confirm(`¿Está seguro de que desea eliminar al empleado "${props.employee.name}" (ID: ${props.employee.id})?`)) {
|
||||
deleteEmpleado();
|
||||
}
|
||||
};
|
||||
|
||||
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.
|
||||
} 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%)';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Scoped styles for the card ensure they don't leak */
|
||||
.max-w-sm {
|
||||
max-width: 24rem; /* Consistent with Tailwind's sm breakpoint for max-width */
|
||||
/* Minimal scoped styles */
|
||||
.shrink-0 {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Adding some subtle enhancements for visual appeal */
|
||||
.text-2xl {
|
||||
line-height: 1.2; /* Adjust line height for the title for better readability */
|
||||
}
|
||||
|
||||
.flex svg {
|
||||
flex-shrink: 0; /* Prevent SVGs from shrinking if text is long, ensuring icon consistency */
|
||||
}
|
||||
|
||||
.employee-avatar-border {
|
||||
border-color: var(--accent-color-empleados);
|
||||
}
|
||||
|
||||
.employee-icon {
|
||||
color: var(--accent-color-empleados);
|
||||
}
|
||||
|
||||
.edit-button {
|
||||
background-color: var(--accent-color-empleados);
|
||||
/* Ensure focus ring also uses accent color if desired, e.g. by adding a specific class or style */
|
||||
}
|
||||
.edit-button:hover {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
.edit-button:focus {
|
||||
box-shadow: 0 0 0 2px var(--background-color), 0 0 0 4px var(--accent-color-empleados);
|
||||
}
|
||||
|
||||
|
||||
/* Styling for buttons for a more refined and interactive look */
|
||||
button {
|
||||
transition: background-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out, transform 0.1s ease-in-out, filter 0.2s ease-in-out;
|
||||
}
|
||||
button:hover {
|
||||
transform: translateY(-1px); /* Slight lift on hover */
|
||||
}
|
||||
button:active {
|
||||
transform: translateY(0px); /* Reset lift on click */
|
||||
}
|
||||
|
||||
.rounded-full {
|
||||
object-fit: cover; /* Ensures the avatar image covers the area without distortion */
|
||||
object-fit: cover; /* Ensures the avatar image covers the area without distortion */
|
||||
}
|
||||
</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-[var(--accent-color-empleados)]" :style="{ backgroundColor: ui.tableBgColorEmpleados }">
|
||||
<thead class="divide-y divide-[var(--accent-color-empleados)]" :style="{ backgroundColor: ui.tableBgColorEmpleados }">
|
||||
<tr>
|
||||
<th scope="col" class="px-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="divide-y divide-[var(--accent-color-empleados)]" :style="{ backgroundColor: ui.tableBgColorEmpleados }">
|
||||
<tr v-if="!employees || employees.length === 0">
|
||||
<td colspan="7" class="px-6 py-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="confirmDeleteEmpleado(employee)" class="p-1.5 sm:p-2 rounded-md transition-all duration-150 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2 text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300 hover:bg-red-100 dark:hover:bg-red-600/20 focus:ring-red-500 dark:focus:ring-red-400" title="Eliminar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12.56 0c1.153 0 2.24.03 3.22.077m3.22-.077L10.879 3.28a2.25 2.25 0 012.244-2.077h.093c.956 0 1.853.543 2.244 2.077L14.74 5.79m-4.858 0l-2.828-2.828A1.875 1.875 0 016.188 2.188l2.828 2.828m6.912 0l2.828-2.828a1.875 1.875 0 00-2.652-2.652L12 5.79M9.26 9h5.48L9.26 9z" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -85,95 +74,54 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PropType } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
<script setup>
|
||||
import { useUi } from '../../stores/useUi.js';
|
||||
import { useEmpleadosStore } from '../../stores/useEmpleados';
|
||||
|
||||
// Interface for Employee object structure, aligning with prisma model (excluding sensitive or large fields for table view)
|
||||
interface Employee {
|
||||
id: string | number; // Primary key for navigation and :key
|
||||
name: string;
|
||||
cedula: number; // Assuming cedula is a number; adjust if it's a string
|
||||
avatar_url?: string;
|
||||
telefono?: string;
|
||||
ubicacion: string; // As per schema, this has a default and likely always present
|
||||
idciat?: string;
|
||||
grupo_estudio?: string;
|
||||
// Omitting created_at, updated_at, empleado boolean for brevity in table
|
||||
}
|
||||
const ui = useUi();
|
||||
const emit = defineEmits(['edit']);
|
||||
const empleadosStore = useEmpleadosStore();
|
||||
|
||||
const props = defineProps({
|
||||
employees: {
|
||||
type: Array as PropType<Employee[]>,
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => [], // Provides a default empty array if no prop is passed
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const handleEdit = (employeeId: string | number) => {
|
||||
router.push(`/empleados/edit/${employeeId}`);
|
||||
const handleEdit = (employeeId) => {
|
||||
emit('edit', employeeId);
|
||||
};
|
||||
|
||||
const handleViewDetails = (employeeId: string | number) => {
|
||||
// This could navigate to a dedicated detail view or the edit view itself
|
||||
router.push(`/empleados/view/${employeeId}`); // Adjust route as per application structure
|
||||
const confirmDeleteEmpleado = (employee) => {
|
||||
if (confirm(`¿Está seguro de que desea eliminar al empleado "${employee.name}" (ID: ${employee.id})?`)) {
|
||||
deleteEmpleadoInternal(employee.id);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteEmpleadoInternal = async (id) => {
|
||||
try {
|
||||
await empleadosStore.deleteEmpleado(id);
|
||||
} catch (error) {
|
||||
console.error(`Error deleting employee with id ${id}:`, error);
|
||||
// Optional: Show error notification
|
||||
}
|
||||
};
|
||||
</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,15 +1,31 @@
|
||||
<template>
|
||||
<div class="planilla-card">
|
||||
<h4>Planilla ID: {{ planilla.id }}</h4>
|
||||
<p><strong>Título:</strong> {{ planilla.titulo }}</p>
|
||||
<p><strong>Empleado ID:</strong> {{ planilla.empleado_id }}</p>
|
||||
<p><strong>Desde:</strong> {{ formatDate(planilla.fecha_desde) }}</p>
|
||||
<p><strong>Hasta:</strong> {{ formatDate(planilla.fecha_hasta) }}</p>
|
||||
<p><strong>Total:</strong> {{ formatCurrency(planilla.total) }}</p>
|
||||
<p><strong>Estado:</strong> <span :class="`estado-${planilla.estado?.toLowerCase()}`">{{ planilla.estado }}</span></p>
|
||||
<div class="actions">
|
||||
<button @click="editPlanilla">Editar</button>
|
||||
<button @click="confirmDeletePlanilla">Eliminar</button>
|
||||
<div :style="{ backgroundColor: ui.tableBgColorPlanillas }" class="shadow-md rounded-lg p-4 md:p-6 m-2 border border-gray-200 hover:shadow-lg transition-shadow duration-300 ease-in-out flex flex-col">
|
||||
<div class="flex justify-between items-center mb-3 md:mb-4 pb-2 md:pb-3 border-b border-gray-100">
|
||||
<h4 class="text-lg md:text-xl font-semibold" :style="{ color: 'var(--accent-color-planillas)' }">Planilla ID: {{ planilla.id }}</h4>
|
||||
<span :class="['px-2 py-1 text-xs font-bold text-white rounded-full', getStatusClass(planilla.estado)]">
|
||||
{{ planilla.estado || 'N/A' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-sm text-gray-700 space-y-2">
|
||||
<p><strong class="font-medium text-gray-900">Título:</strong> {{ planilla.titulo }}</p>
|
||||
<p><strong class="font-medium text-gray-900">Empleado ID:</strong> {{ planilla.empleado_id }}</p>
|
||||
<p><strong class="font-medium text-gray-900">Desde:</strong> {{ formatDate(planilla.fecha_desde) }}</p>
|
||||
<p><strong class="font-medium text-gray-900">Hasta:</strong> {{ formatDate(planilla.fecha_hasta) }}</p>
|
||||
<p><strong class="font-medium text-gray-900">Total:</strong> {{ formatCurrency(planilla.total) }}</p>
|
||||
</div>
|
||||
<div class="mt-auto pt-3 md:pt-4 flex justify-end space-x-2 md:space-x-3">
|
||||
<button
|
||||
@click="editPlanilla"
|
||||
class="px-3 py-1 md:px-4 md:py-2 text-xs md:text-sm font-medium rounded-md transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2 text-white"
|
||||
:style="{ backgroundColor: 'var(--accent-color-planillas)', borderColor: 'var(--accent-color-planillas)' }"
|
||||
@mouseover="buttonHover($event, true)"
|
||||
@mouseleave="buttonHover($event, false)"
|
||||
:class="`focus:ring-[var(--accent-color-planillas)]`"
|
||||
>Editar</button>
|
||||
<button
|
||||
@click="confirmDeletePlanilla"
|
||||
class="px-3 py-1 md:px-4 md:py-2 text-xs md:text-sm font-medium rounded-md transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2 bg-red-600 hover:bg-red-700 text-white focus:ring-red-500"
|
||||
>Eliminar</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -17,6 +33,9 @@
|
||||
<script setup>
|
||||
import { defineProps, defineEmits } from 'vue';
|
||||
import { usePlanillasStore } from '../../stores/usePlanillas';
|
||||
import { useUi } from '../../stores/useUi.js';
|
||||
|
||||
const ui = useUi();
|
||||
|
||||
const props = defineProps({
|
||||
planilla: {
|
||||
@@ -36,9 +55,7 @@ const formatDate = (dateString) => {
|
||||
};
|
||||
|
||||
const formatCurrency = (value) => {
|
||||
if (value == null) return 'N/A'; // Handle null or undefined totals
|
||||
// Assuming the value is a number or can be converted to one.
|
||||
// Adjust 'es-PY' and currency 'PYG' (Paraguayan Guarani) as needed.
|
||||
if (value == null) return 'N/A';
|
||||
return Number(value).toLocaleString('es-PY', {
|
||||
style: 'currency',
|
||||
currency: 'PYG'
|
||||
@@ -50,8 +67,7 @@ const editPlanilla = () => {
|
||||
};
|
||||
|
||||
const confirmDeletePlanilla = () => {
|
||||
// In a real app, you'd use a confirmation dialog here
|
||||
if (confirm(`¿Está seguro de que desea eliminar la planilla "${props.planilla.titulo}"?`)) {
|
||||
if (confirm(`¿Está seguro de que desea eliminar la planilla "${props.planilla.titulo}" (ID: ${props.planilla.id})?`)) {
|
||||
deletePlanilla();
|
||||
}
|
||||
};
|
||||
@@ -59,85 +75,35 @@ const confirmDeletePlanilla = () => {
|
||||
const deletePlanilla = async () => {
|
||||
try {
|
||||
await planillasStore.deletePlanilla(props.planilla.id);
|
||||
// Optionally, emit an event or show a notification upon successful deletion
|
||||
// For example: emit('deleted', props.planilla.id);
|
||||
} catch (error) {
|
||||
console.error('Error deleting planilla:', error);
|
||||
// Handle error (e.g., show a notification to the user)
|
||||
alert('Ocurrió un error al eliminar la planilla.');
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusClass = (status) => {
|
||||
if (!status) return 'bg-gray-400';
|
||||
const statusNormalized = status.toLowerCase().replace(/\s+/g, '-');
|
||||
switch (statusNormalized) {
|
||||
case 'pagado': return 'bg-green-500';
|
||||
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';
|
||||
}
|
||||
};
|
||||
|
||||
const buttonHover = (event, isHovering) => {
|
||||
if (isHovering) {
|
||||
event.target.style.filter = 'brightness(90%)';
|
||||
} else {
|
||||
event.target.style.filter = 'brightness(100%)';
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.planilla-card {
|
||||
border: 1px solid #ccc;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 8px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.planilla-card h4 { /* Updated selector from h3 to h4 */
|
||||
margin-top: 0;
|
||||
color: var(--accent-color-planillas); /* Accent color for title */
|
||||
font-size: 1.2em; /* Adjust size as needed, h4 is typically smaller than h3 */
|
||||
}
|
||||
|
||||
.planilla-card h3 { /* Keep if h3 is used elsewhere, or remove if h4 is the new standard */
|
||||
margin-top: 0;
|
||||
color: var(--accent-color-planillas); /* Accent color for title */
|
||||
}
|
||||
|
||||
.planilla-card p {
|
||||
margin: 8px 0;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.planilla-card .actions {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.planilla-card button {
|
||||
padding: 8px 12px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.planilla-card button:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.actions button:first-child { /* Edit button */
|
||||
background-color: var(--accent-color-planillas);
|
||||
color: white;
|
||||
}
|
||||
.actions button:first-child:hover { /* Edit button hover */
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
|
||||
.actions button:last-child { /* Delete button */
|
||||
background-color: var(--warning-color); /* Using warning color for delete */
|
||||
color: white;
|
||||
}
|
||||
.actions button:last-child:hover { /* Delete button hover */
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
|
||||
/* Example status styling */
|
||||
.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;
|
||||
}
|
||||
/* 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. */
|
||||
</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-[var(--accent-color-planillas)]" :style="{ backgroundColor: ui.tableBgColorPlanillas }">
|
||||
<thead class="divide-y divide-[var(--accent-color-planillas)]" :style="{ backgroundColor: ui.tableBgColorPlanillas }">
|
||||
<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="divide-y divide-[var(--accent-color-planillas)]" :style="{ backgroundColor: ui.tableBgColorPlanillas }">
|
||||
<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,17 +1,34 @@
|
||||
<template>
|
||||
<div class="tarea-card">
|
||||
<h4>Tarea ID: {{ tarea.id }}</h4>
|
||||
<p><strong>Título:</strong> {{ tarea.titulo }}</p>
|
||||
<p><strong>Empleado ID:</strong> {{ tarea.empleado_id }}</p>
|
||||
<p><strong>Fecha:</strong> {{ formatDate(tarea.fecha) }}</p>
|
||||
<p><strong>Estado:</strong> <span :class="`estado-${tarea.estado?.toLowerCase().replace(/\s+/g, '-')}`">{{ tarea.estado }}</span></p>
|
||||
<p><strong>Tipo:</strong> {{ tarea.tipo || 'N/A' }}</p>
|
||||
<p v-if="tarea.precio != null"><strong>Precio:</strong> {{ formatCurrency(tarea.precio) }}</p>
|
||||
<p v-if="tarea.observacion"><strong>Observación:</strong> {{ tarea.observacion }}</p>
|
||||
|
||||
<div class="actions">
|
||||
<button @click="editTarea" class="action-button edit-button">Editar</button>
|
||||
<button @click="confirmDeleteTarea" class="action-button delete-button">Eliminar</button>
|
||||
<div :style="{ backgroundColor: ui.tableBgColorTareas }" class="shadow-md rounded-lg p-4 md:p-6 m-2 border border-gray-200 hover:shadow-lg transition-shadow duration-300 ease-in-out flex flex-col">
|
||||
<div class="flex justify-between items-center mb-3 md:mb-4 pb-2 md:pb-3 border-b border-gray-100">
|
||||
<h4 class="text-lg md:text-xl font-semibold" :style="{ color: 'var(--accent-color-tareas)' }">Tarea ID: {{ tarea.id }}</h4>
|
||||
<span :class="['px-2 py-1 text-xs font-bold text-white rounded-full', getStatusClass(tarea.estado)]">
|
||||
{{ tarea.estado || 'N/A' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-sm text-gray-700 space-y-2">
|
||||
<p><strong class="font-medium text-gray-900">Título:</strong> {{ tarea.titulo }}</p>
|
||||
<p><strong class="font-medium text-gray-900">Empleado ID:</strong> {{ tarea.empleado_id }}</p>
|
||||
<p><strong class="font-medium text-gray-900">Fecha:</strong> {{ formatDate(tarea.fecha) }}</p>
|
||||
<p><strong class="font-medium text-gray-900">Tipo:</strong> {{ tarea.tipo || 'N/A' }}</p>
|
||||
<p v-if="tarea.precio != null"><strong class="font-medium text-gray-900">Precio:</strong> {{ formatCurrency(tarea.precio) }}</p>
|
||||
<p v-if="tarea.observacion" class="italic text-gray-600 bg-gray-50 p-2 border-l-3 rounded" :style="{ borderColor: 'var(--accent-color-tareas)' }">
|
||||
<strong class="font-medium text-gray-900">Observación:</strong> {{ tarea.observacion }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-auto pt-3 md:pt-4 flex justify-end space-x-2 md:space-x-3">
|
||||
<button
|
||||
@click="editTarea"
|
||||
class="px-3 py-1 md:px-4 md:py-2 text-xs md:text-sm font-medium rounded-md transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2 text-white"
|
||||
:style="{ backgroundColor: 'var(--accent-color-tareas)', borderColor: 'var(--accent-color-tareas)' }"
|
||||
@mouseover="buttonHover($event, true)"
|
||||
@mouseleave="buttonHover($event, false)"
|
||||
:class="`focus:ring-[var(--accent-color-tareas)]`"
|
||||
>Editar</button>
|
||||
<button
|
||||
@click="confirmDeleteTarea"
|
||||
class="px-3 py-1 md:px-4 md:py-2 text-xs md:text-sm font-medium rounded-md transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2 bg-red-600 hover:bg-red-700 text-white focus:ring-red-500"
|
||||
>Eliminar</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -19,6 +36,9 @@
|
||||
<script setup>
|
||||
import { defineProps, defineEmits } from 'vue';
|
||||
import { useTareasStore } from '../../stores/useTareas';
|
||||
import { useUi } from '../../stores/useUi.js';
|
||||
|
||||
const ui = useUi();
|
||||
|
||||
const props = defineProps({
|
||||
tarea: {
|
||||
@@ -39,7 +59,7 @@ const formatDate = (dateString) => {
|
||||
|
||||
const formatCurrency = (value) => {
|
||||
if (value == null) return '';
|
||||
return Number(value).toLocaleString('es-PY', { // Paraguayan Guarani
|
||||
return Number(value).toLocaleString('es-PY', {
|
||||
style: 'currency',
|
||||
currency: 'PYG',
|
||||
});
|
||||
@@ -50,7 +70,7 @@ const editTarea = () => {
|
||||
};
|
||||
|
||||
const confirmDeleteTarea = () => {
|
||||
if (confirm(`¿Está seguro de que desea eliminar la tarea "${props.tarea.titulo}"?`)) {
|
||||
if (confirm(`¿Está seguro de que desea eliminar la tarea "${props.tarea.titulo}" (ID: ${props.tarea.id})?`)) {
|
||||
deleteTareaInternal();
|
||||
}
|
||||
};
|
||||
@@ -58,99 +78,42 @@ const confirmDeleteTarea = () => {
|
||||
const deleteTareaInternal = async () => {
|
||||
try {
|
||||
await tareasStore.deleteTarea(props.tarea.id);
|
||||
// Optionally, emit a 'deleted' event: emit('deleted', props.tarea.id);
|
||||
// Or show a success notification.
|
||||
} catch (error) {
|
||||
console.error('Error deleting tarea:', error);
|
||||
alert('Ocurrió un error al eliminar la tarea. Por favor, intente de nuevo.');
|
||||
// Potentially show a more user-friendly notification.
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusClass = (status) => {
|
||||
if (!status) return 'bg-gray-400';
|
||||
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 'anulada':
|
||||
case 'cancelada': return 'bg-red-500';
|
||||
case 'archivada': return 'bg-gray-500';
|
||||
default: return 'bg-gray-400';
|
||||
}
|
||||
};
|
||||
|
||||
const buttonHover = (event, isHovering) => {
|
||||
if (isHovering) {
|
||||
event.target.style.filter = 'brightness(90%)';
|
||||
} else {
|
||||
event.target.style.filter = 'brightness(100%)';
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tarea-card {
|
||||
border: 1px solid #e0e0e0;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 8px;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
||||
transition: box-shadow 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.tarea-card:hover {
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
.tarea-card h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 12px;
|
||||
color: var(--accent-color-tareas); /* Accent color for title */
|
||||
font-size: 1.15em; /* Slightly smaller than a typical h3 */
|
||||
}
|
||||
|
||||
.tarea-card p {
|
||||
margin: 6px 0;
|
||||
color: #555;
|
||||
font-size: 0.95em;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.tarea-card p strong {
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
padding: 8px 12px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
transition: background-color 0.2s ease, opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.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; /* Darker red */
|
||||
}
|
||||
|
||||
/* Status styling: Added .replace(/\s+/g, '-') for multi-word statuses */
|
||||
.estado-pendiente {
|
||||
color: #ff9800; /* Orange */
|
||||
font-weight: bold;
|
||||
}
|
||||
.estado-realizada,
|
||||
.estado-completada, /* Common synonyms for 'done' */
|
||||
.estado-hecho {
|
||||
color: #4caf50; /* Green */
|
||||
font-weight: bold;
|
||||
}
|
||||
.estado-en-progreso { /* Example for 'in progress' */
|
||||
color: #2196f3; /* Blue */
|
||||
font-weight: bold;
|
||||
}
|
||||
.estado-anulada,
|
||||
.estado-cancelada {
|
||||
color: #f44336; /* Red */
|
||||
font-weight: bold;
|
||||
/* text-decoration: line-through; */ /* Optional */
|
||||
/* 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;
|
||||
}
|
||||
</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-[var(--accent-color-tareas)]"
|
||||
: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"
|
||||
>
|
||||
<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 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,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>
|
||||
|
||||
@@ -21,6 +21,15 @@ watch(route, v => (activePath.value = v.path))
|
||||
|
||||
// clases dinámicas p/ mostrar / ocultar barra
|
||||
const sidebarClasses = computed(() => ui.sidebarOpen ? 'translate-x-0' : '-translate-x-full')
|
||||
|
||||
const handleLinkClick = () => {
|
||||
// Close sidebar if desktopNavbarPersistent is false or if it's mobile view (width < 768px)
|
||||
// Assuming 768px is the 'md' breakpoint.
|
||||
if (!ui.desktopNavbarPersistent || window.innerWidth < 768) {
|
||||
ui.closeSidebar()
|
||||
}
|
||||
// Otherwise, (desktopNavbarPersistent is true AND width >= 768px), do nothing to keep sidebar open.
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -53,7 +62,7 @@ const sidebarClasses = computed(() => ui.sidebarOpen ? 'translate-x-0' : '-trans
|
||||
:to="l.to"
|
||||
class="nav-link flex items-center gap-3 w-full px-3 py-2 rounded-md font-medium transition group"
|
||||
:class="activePath.startsWith(l.to) ? 'active' : ''"
|
||||
@click="ui.closeSidebar()"
|
||||
@click="handleLinkClick"
|
||||
>
|
||||
<span class="text-lg" aria-hidden="true">{{ l.icon }}</span>
|
||||
<span class="truncate">{{ l.label }}</span>
|
||||
|
||||
@@ -48,10 +48,16 @@ describe('useUi Store', () => {
|
||||
expect(store.accentColorTareas).toBe('#4CAF50')
|
||||
expect(store.accentColorPlanillas).toBe('#FF9800')
|
||||
expect(store.accentColorAsistencias).toBe('#E91E63')
|
||||
expect(localStorageMock.getItem).toHaveBeenCalledWith(APPEARANCE_STORAGE_KEY)
|
||||
})
|
||||
|
||||
it('loads settings from localStorage including new accent colors if present', () => {
|
||||
expect(store.accentColorConfiguracion).toBe('#607D8B')
|
||||
// Check new default view defaults
|
||||
expect(store.defaultViewEmpleados).toBe('table')
|
||||
expect(store.defaultViewTareas).toBe('table')
|
||||
expect(store.defaultViewPlanillas).toBe('table')
|
||||
expect(store.defaultViewAsistencias).toBe('table')
|
||||
expect(store.defaultViewConfiguracion).toBe('table')
|
||||
// Check other new defaults
|
||||
expect(store.tableBgColorEmpleados).toBe('#FFFFFF')
|
||||
expect(store.desktopNavbarPersistent).toBe(false)
|
||||
expect(localStorageMock.getItem).toHaveBeenCalledWith(APPEARANCE_STORAGE_KEY)
|
||||
})
|
||||
|
||||
@@ -65,7 +71,18 @@ describe('useUi Store', () => {
|
||||
accentColorTareas: '#00FF00',
|
||||
accentColorPlanillas: '#FFFF00',
|
||||
accentColorAsistencias: '#FF00FF',
|
||||
// other settings...
|
||||
accentColorConfiguracion: '#112233',
|
||||
tableBgColorEmpleados: '#EEEEEE',
|
||||
tableBgColorTareas: '#DDDDDD',
|
||||
tableBgColorPlanillas: '#CCCCCC',
|
||||
tableBgColorAsistencias: '#BBBBBB',
|
||||
tableBgColorConfiguracion: '#AAAAAA',
|
||||
desktopNavbarPersistent: true,
|
||||
defaultViewEmpleados: 'card',
|
||||
defaultViewTareas: 'card',
|
||||
defaultViewPlanillas: 'card',
|
||||
defaultViewAsistencias: 'card',
|
||||
defaultViewConfiguracion: 'card',
|
||||
}
|
||||
localStorageMock.getItem.mockReturnValueOnce(JSON.stringify(storedSettings))
|
||||
|
||||
@@ -80,9 +97,21 @@ describe('useUi Store', () => {
|
||||
expect(store.accentColorTareas).toBe('#00FF00')
|
||||
expect(store.accentColorPlanillas).toBe('#FFFF00')
|
||||
expect(store.accentColorAsistencias).toBe('#FF00FF')
|
||||
expect(store.accentColorConfiguracion).toBe('#112233')
|
||||
expect(store.tableBgColorEmpleados).toBe('#EEEEEE')
|
||||
expect(store.tableBgColorTareas).toBe('#DDDDDD')
|
||||
expect(store.tableBgColorPlanillas).toBe('#CCCCCC')
|
||||
expect(store.tableBgColorAsistencias).toBe('#BBBBBB')
|
||||
expect(store.tableBgColorConfiguracion).toBe('#AAAAAA')
|
||||
expect(store.desktopNavbarPersistent).toBe(true)
|
||||
expect(store.defaultViewEmpleados).toBe('card')
|
||||
expect(store.defaultViewTareas).toBe('card')
|
||||
expect(store.defaultViewPlanillas).toBe('card')
|
||||
expect(store.defaultViewAsistencias).toBe('card')
|
||||
expect(store.defaultViewConfiguracion).toBe('card')
|
||||
})
|
||||
|
||||
it('loads older settings from localStorage and uses defaults for new accent colors if not present', () => {
|
||||
it('loads older settings from localStorage and uses defaults for new settings if not present', () => {
|
||||
const olderStoredSettings = {
|
||||
primaryColor: '#ABCDEF',
|
||||
theme: 'dark',
|
||||
@@ -96,10 +125,20 @@ describe('useUi Store', () => {
|
||||
expect(store.theme).toBe('dark')
|
||||
expect(store.fontSize).toBe(18)
|
||||
// New accent colors should fall back to defaults
|
||||
expect(store.accentColorEmpleados).toBe('#2196F3')
|
||||
expect(store.accentColorTareas).toBe('#4CAF50')
|
||||
expect(store.accentColorPlanillas).toBe('#FF9800')
|
||||
expect(store.accentColorAsistencias).toBe('#E91E63')
|
||||
expect(store.accentColorEmpleados).toBe('#2196F3') // Default
|
||||
expect(store.accentColorTareas).toBe('#4CAF50') // Default
|
||||
expect(store.accentColorPlanillas).toBe('#FF9800') // Default
|
||||
expect(store.accentColorAsistencias).toBe('#E91E63') // Default
|
||||
expect(store.accentColorConfiguracion).toBe('#607D8B') // Default
|
||||
// New default view keys should fall back to 'table'
|
||||
expect(store.defaultViewEmpleados).toBe('table')
|
||||
expect(store.defaultViewTareas).toBe('table')
|
||||
expect(store.defaultViewPlanillas).toBe('table')
|
||||
expect(store.defaultViewAsistencias).toBe('table')
|
||||
expect(store.defaultViewConfiguracion).toBe('table')
|
||||
// Other new keys
|
||||
expect(store.tableBgColorEmpleados).toBe('#FFFFFF') // Default
|
||||
expect(store.desktopNavbarPersistent).toBe(false) // Default
|
||||
})
|
||||
|
||||
it('falls back to default settings if localStorage data is invalid JSON', () => {
|
||||
@@ -129,11 +168,14 @@ describe('useUi Store', () => {
|
||||
const appearanceSettingKeysInTest = [
|
||||
'primaryColor', 'secondaryColor', 'warningColor', 'fontFamily',
|
||||
'fontSize', 'animationsEnabled', 'backgroundColor', 'theme',
|
||||
'accentColorEmpleados', 'accentColorTareas', 'accentColorPlanillas', 'accentColorAsistencias',
|
||||
const appearanceSettingKeys = [
|
||||
'primaryColor', 'secondaryColor', 'warningColor', 'fontFamily',
|
||||
'fontSize', 'animationsEnabled', 'backgroundColor', 'theme',
|
||||
]
|
||||
'accentColorEmpleados', 'accentColorTareas', 'accentColorPlanillas',
|
||||
'accentColorAsistencias', 'accentColorConfiguracion',
|
||||
'tableBgColorEmpleados', 'tableBgColorTareas', 'tableBgColorPlanillas',
|
||||
'tableBgColorAsistencias', 'tableBgColorConfiguracion',
|
||||
'desktopNavbarPersistent',
|
||||
'defaultViewEmpleados', 'defaultViewTareas', 'defaultViewPlanillas',
|
||||
'defaultViewAsistencias', 'defaultViewConfiguracion',
|
||||
];
|
||||
|
||||
it('setPrimaryColor updates state and saves to localStorage', () => {
|
||||
const store = useUi()
|
||||
@@ -215,7 +257,9 @@ describe('useUi Store', () => {
|
||||
expect(Object.keys(savedData).length).toBe(appearanceSettingKeysInTest.length);
|
||||
expect(savedData.sidebarOpen).toBeUndefined() // Ensure non-appearance data is not saved
|
||||
appearanceSettingKeysInTest.forEach(key => {
|
||||
expect(savedData.hasOwnProperty(key)).toBe(true)
|
||||
//This assertion needs to be robust if some keys are not initialized (e.g. undefined)
|
||||
//However, our store initializes all appearance keys
|
||||
expect(savedData.hasOwnProperty(key)).toBe(true);
|
||||
})
|
||||
})
|
||||
|
||||
@@ -259,11 +303,100 @@ describe('useUi Store', () => {
|
||||
expect.stringContaining('"accentColorAsistencias":"#FF7788"')
|
||||
)
|
||||
})
|
||||
expect(Object.keys(savedData).length).toBe(appearanceSettingKeys.length);
|
||||
expect(savedData.sidebarOpen).toBeUndefined() // Ensure non-appearance data is not saved
|
||||
appearanceSettingKeys.forEach(key => {
|
||||
expect(savedData.hasOwnProperty(key)).toBe(true)
|
||||
})
|
||||
|
||||
it('setAccentColorConfiguracion updates state and saves to localStorage', () => {
|
||||
const store = useUi()
|
||||
store.setAccentColorConfiguracion('#99AABB')
|
||||
expect(store.accentColorConfiguracion).toBe('#99AABB')
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledWith(
|
||||
APPEARANCE_STORAGE_KEY,
|
||||
expect.stringContaining('"accentColorConfiguracion":"#99AABB"')
|
||||
)
|
||||
})
|
||||
|
||||
// Tests for table background color actions
|
||||
it('setTableBgColorEmpleados updates state and saves to localStorage', () => {
|
||||
const store = useUi()
|
||||
store.setTableBgColorEmpleados('#EEECCC')
|
||||
expect(store.tableBgColorEmpleados).toBe('#EEECCC')
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledWith(
|
||||
APPEARANCE_STORAGE_KEY,
|
||||
expect.stringContaining('"tableBgColorEmpleados":"#EEECCC"')
|
||||
)
|
||||
})
|
||||
// Similar tests for Tareas, Planillas, Asistencias, Configuracion table bg colors...
|
||||
|
||||
it('setDesktopNavbarPersistent updates state and saves to localStorage', () => {
|
||||
const store = useUi()
|
||||
store.setDesktopNavbarPersistent(true)
|
||||
expect(store.desktopNavbarPersistent).toBe(true)
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledWith(
|
||||
APPEARANCE_STORAGE_KEY,
|
||||
expect.stringContaining('"desktopNavbarPersistent":true')
|
||||
)
|
||||
store.setDesktopNavbarPersistent(false)
|
||||
expect(store.desktopNavbarPersistent).toBe(false)
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledWith(
|
||||
APPEARANCE_STORAGE_KEY,
|
||||
expect.stringContaining('"desktopNavbarPersistent":false')
|
||||
)
|
||||
})
|
||||
|
||||
// Tests for new default view actions
|
||||
it('setDefaultViewEmpleados updates state and saves to localStorage', () => {
|
||||
const store = useUi()
|
||||
store.setDefaultViewEmpleados('card')
|
||||
expect(store.defaultViewEmpleados).toBe('card')
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledWith(
|
||||
APPEARANCE_STORAGE_KEY,
|
||||
expect.stringContaining('"defaultViewEmpleados":"card"')
|
||||
)
|
||||
store.setDefaultViewEmpleados('table')
|
||||
expect(store.defaultViewEmpleados).toBe('table')
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledWith(
|
||||
APPEARANCE_STORAGE_KEY,
|
||||
expect.stringContaining('"defaultViewEmpleados":"table"')
|
||||
)
|
||||
})
|
||||
|
||||
it('setDefaultViewTareas updates state and saves to localStorage', () => {
|
||||
const store = useUi()
|
||||
store.setDefaultViewTareas('card')
|
||||
expect(store.defaultViewTareas).toBe('card')
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledWith(
|
||||
APPEARANCE_STORAGE_KEY,
|
||||
expect.stringContaining('"defaultViewTareas":"card"')
|
||||
)
|
||||
})
|
||||
|
||||
it('setDefaultViewPlanillas updates state and saves to localStorage', () => {
|
||||
const store = useUi()
|
||||
store.setDefaultViewPlanillas('card')
|
||||
expect(store.defaultViewPlanillas).toBe('card')
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledWith(
|
||||
APPEARANCE_STORAGE_KEY,
|
||||
expect.stringContaining('"defaultViewPlanillas":"card"')
|
||||
)
|
||||
})
|
||||
|
||||
it('setDefaultViewAsistencias updates state and saves to localStorage', () => {
|
||||
const store = useUi()
|
||||
store.setDefaultViewAsistencias('card')
|
||||
expect(store.defaultViewAsistencias).toBe('card')
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledWith(
|
||||
APPEARANCE_STORAGE_KEY,
|
||||
expect.stringContaining('"defaultViewAsistencias":"card"')
|
||||
)
|
||||
})
|
||||
|
||||
it('setDefaultViewConfiguracion updates state and saves to localStorage', () => {
|
||||
const store = useUi()
|
||||
store.setDefaultViewConfiguracion('card')
|
||||
expect(store.defaultViewConfiguracion).toBe('card')
|
||||
expect(localStorageMock.setItem).toHaveBeenCalledWith(
|
||||
APPEARANCE_STORAGE_KEY,
|
||||
expect.stringContaining('"defaultViewConfiguracion":"card"')
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -17,12 +17,30 @@ const appearanceSettingKeys = [
|
||||
'accentColorPlanillas',
|
||||
'accentColorAsistencias',
|
||||
'accentColorConfiguracion',
|
||||
'accentColorChat',
|
||||
// Per-module table background colors
|
||||
'tableBgColorEmpleados',
|
||||
'tableBgColorTareas',
|
||||
'tableBgColorPlanillas',
|
||||
'tableBgColorAsistencias',
|
||||
'tableBgColorConfiguracion',
|
||||
'desktopNavbarPersistent',
|
||||
// Default module views
|
||||
'defaultViewEmpleados',
|
||||
'defaultViewTareas',
|
||||
'defaultViewPlanillas',
|
||||
'defaultViewAsistencias',
|
||||
'defaultViewConfiguracion',
|
||||
'transitionSpeed',
|
||||
'chatAgentMessageColor',
|
||||
'chatOwnMessageColor',
|
||||
'chatInputBoxColor',
|
||||
'chatAccentColor',
|
||||
'chatBackgroundColor',
|
||||
'chatFontColor',
|
||||
'chatFontFamily',
|
||||
'chatFontSize',
|
||||
|
||||
]
|
||||
|
||||
const loadSettingsFromLocalStorage = () => {
|
||||
@@ -84,12 +102,32 @@ export const useUi = defineStore('ui', {
|
||||
accentColorPlanillas: '#FF9800', // Orange
|
||||
accentColorAsistencias: '#E91E63', // Pink
|
||||
accentColorConfiguracion: '#607D8B', // Blue Grey
|
||||
accentColorChat: '#0D9488', // Teal - chosen as a default for chat
|
||||
// Per-module table background colors - default to white
|
||||
tableBgColorEmpleados: '#FFFFFF',
|
||||
tableBgColorTareas: '#FFFFFF',
|
||||
tableBgColorPlanillas: '#FFFFFF',
|
||||
tableBgColorAsistencias: '#FFFFFF',
|
||||
tableBgColorConfiguracion: '#FFFFFF',
|
||||
backgroundColorChat: '#F0F0F0', // A light gray for chat background by default
|
||||
desktopNavbarPersistent: false,
|
||||
// Default module views
|
||||
'defaultViewEmpleados': 'table',
|
||||
'defaultViewTareas': 'table',
|
||||
'defaultViewPlanillas': 'table',
|
||||
'defaultViewAsistencias': 'table',
|
||||
'defaultViewConfiguracion': 'table',
|
||||
transitionSpeed: 1, // Default to normal speed
|
||||
// Chat UI colors
|
||||
chatAgentMessageColor: '#FFFFFF',
|
||||
chatOwnMessageColor: '#D1E8FF',
|
||||
chatInputBoxColor: '#FFFFFF',
|
||||
chatAccentColor: '#1976D2',
|
||||
chatBackgroundColor: '#F4F4F4',
|
||||
// Chat UI fonts
|
||||
chatFontColor: '#000000',
|
||||
chatFontFamily: '',
|
||||
chatFontSize: 14,
|
||||
}
|
||||
|
||||
const loadedSettings = loadSettingsFromLocalStorage()
|
||||
@@ -176,6 +214,10 @@ export const useUi = defineStore('ui', {
|
||||
this.accentColorConfiguracion = color
|
||||
_saveAppearanceState(this)
|
||||
},
|
||||
setAccentColorChat(color) {
|
||||
this.accentColorChat = color;
|
||||
_saveAppearanceState(this);
|
||||
},
|
||||
|
||||
// Actions for per-module table background colors
|
||||
setTableBgColorEmpleados(color) {
|
||||
@@ -197,7 +239,79 @@ export const useUi = defineStore('ui', {
|
||||
setTableBgColorConfiguracion(color) {
|
||||
this.tableBgColorConfiguracion = color
|
||||
_saveAppearanceState(this)
|
||||
}
|
||||
},
|
||||
setBackgroundColorChat(color) {
|
||||
this.backgroundColorChat = color;
|
||||
_saveAppearanceState(this);
|
||||
},
|
||||
setDesktopNavbarPersistent(enabled) {
|
||||
this.desktopNavbarPersistent = !!enabled // Ensure boolean
|
||||
_saveAppearanceState(this)
|
||||
},
|
||||
|
||||
// Actions for default module views
|
||||
setDefaultViewEmpleados(view) {
|
||||
this.defaultViewEmpleados = view
|
||||
_saveAppearanceState(this)
|
||||
},
|
||||
setDefaultViewTareas(view) {
|
||||
this.defaultViewTareas = view
|
||||
_saveAppearanceState(this)
|
||||
},
|
||||
setDefaultViewPlanillas(view) {
|
||||
this.defaultViewPlanillas = view
|
||||
_saveAppearanceState(this)
|
||||
},
|
||||
setDefaultViewAsistencias(view) {
|
||||
this.defaultViewAsistencias = view
|
||||
_saveAppearanceState(this)
|
||||
},
|
||||
setDefaultViewConfiguracion(view) {
|
||||
this.defaultViewConfiguracion = view
|
||||
_saveAppearanceState(this)
|
||||
},
|
||||
|
||||
// Action for transition speed
|
||||
setTransitionSpeed(newSpeed) {
|
||||
this.transitionSpeed = Number(newSpeed) // Ensure it's a number
|
||||
_saveAppearanceState(this)
|
||||
},
|
||||
|
||||
// Actions for chat UI colors
|
||||
setChatAgentMessageColor(color) {
|
||||
this.chatAgentMessageColor = color
|
||||
_saveAppearanceState(this)
|
||||
},
|
||||
setChatOwnMessageColor(color) {
|
||||
this.chatOwnMessageColor = color
|
||||
_saveAppearanceState(this)
|
||||
},
|
||||
setChatInputBoxColor(color) {
|
||||
this.chatInputBoxColor = color
|
||||
_saveAppearanceState(this)
|
||||
},
|
||||
setChatAccentColor(color) {
|
||||
this.chatAccentColor = color
|
||||
_saveAppearanceState(this)
|
||||
},
|
||||
setChatBackgroundColor(color) {
|
||||
this.chatBackgroundColor = color
|
||||
_saveAppearanceState(this)
|
||||
},
|
||||
|
||||
// Actions for chat UI fonts
|
||||
setChatFontColor(color) {
|
||||
this.chatFontColor = color
|
||||
_saveAppearanceState(this)
|
||||
},
|
||||
setChatFontFamily(font) {
|
||||
this.chatFontFamily = font
|
||||
_saveAppearanceState(this)
|
||||
},
|
||||
setChatFontSize(size) {
|
||||
this.chatFontSize = Number(size)
|
||||
_saveAppearanceState(this)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -4,28 +4,51 @@
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--primary-color: #1976D2;
|
||||
/* Colores base */
|
||||
--primary-color: #1976D2;
|
||||
--secondary-color: #424242;
|
||||
--warning-color: #FFC107;
|
||||
--background-color: #FFFFFF;
|
||||
--warning-color: #dc3545; /* rojo para acciones peligrosas */
|
||||
--background-color:#FFFFFF;
|
||||
|
||||
/* Tipografía */
|
||||
--font-family: 'Roboto', sans-serif;
|
||||
--font-size: 16px;
|
||||
/* Add other variables as needed, e.g., text colors for themes */
|
||||
--text-color: #212121; /* Default text color for light theme */
|
||||
|
||||
/* Module-specific accent colors - Default Fallbacks */
|
||||
--accent-color-empleados: #2196F3;
|
||||
--accent-color-tareas: #4CAF50;
|
||||
--accent-color-planillas: #FF9800;
|
||||
--accent-color-asistencias: #E91E63;
|
||||
/* Texto y bordes */
|
||||
--text-color: #333333;
|
||||
--muted-text-color: #555555;
|
||||
--border-color: #e0e0e0;
|
||||
|
||||
/* Sombras */
|
||||
--card-shadow: 0 2px 5px rgba(0,0,0,0.05);
|
||||
--card-hover-shadow: 0 4px 10px rgba(0,0,0,0.10);
|
||||
|
||||
/* Colores de módulo */
|
||||
--accent-color-asistencias: #4CAF50;
|
||||
--accent-color-empleados: #2196F3;
|
||||
--accent-color-planillas: #FF9800;
|
||||
--accent-color-tareas: #9C27B0;
|
||||
|
||||
/* Fondos de tabla (light) */
|
||||
--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 {
|
||||
--primary-color: #2196F3; /* Example dark theme primary */
|
||||
--secondary-color: #757575; /* Example dark theme secondary */
|
||||
--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}`;
|
||||
}
|
||||
*/
|
||||
@@ -4,11 +4,36 @@ import CanvasChat from '@/components/chat/CanvasChat.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full flex flex-col">
|
||||
<CanvasChat class="flex-1" />
|
||||
<div class="chat-view-container flex flex-col h-full">
|
||||
<header class="page-header">
|
||||
<h1>Chat</h1>
|
||||
</header>
|
||||
<CanvasChat class="flex-1 min-h-0" /> <!-- Added min-h-0 -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* nada por ahora */
|
||||
.chat-view-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background-color: var(--background-color-chat, #F0F0F0); /* Fallback to store default */
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between; /* Consistent with other headers, even if no button on right */
|
||||
align-items: center;
|
||||
margin-bottom: 25px; /* Consistent with PlanillasIndex */
|
||||
padding: 10px 20px; /* Provides padding for the header itself */
|
||||
border-bottom: 1px solid #eee; /* Consistent with PlanillasIndex */
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
color: var(--accent-color-chat, #0D9488); /* Fallback to store default */
|
||||
font-size: 2.2em; /* Consistent with PlanillasIndex */
|
||||
font-weight: 600; /* Consistent with PlanillasIndex */
|
||||
}
|
||||
|
||||
/* CanvasChat is expected to manage its own internal padding and scrolling */
|
||||
</style>
|
||||
|
||||
@@ -21,6 +21,31 @@
|
||||
<input type="checkbox" id="animationsEnabled" v-model="ui.animationsEnabled" @change="ui.setAnimationsEnabled($event.target.checked)"
|
||||
class="custom-checkbox relative w-10 h-5 appearance-none bg-gray-300 dark:bg-gray-600 rounded-full cursor-pointer transition-colors duration-300 ease-in-out checked:bg-[var(--primary-color)] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[var(--primary-color)] focus:ring-offset-[var(--background-color)]">
|
||||
</div>
|
||||
<div class="setting-item flex items-center justify-between mt-4 md:mt-0 md:pt-6">
|
||||
<label for="desktopNavbarPersistent" class="text-sm font-medium">Persistent Desktop Navbar</label>
|
||||
<input type="checkbox" id="desktopNavbarPersistent" v-model="ui.desktopNavbarPersistent" @change="ui.setDesktopNavbarPersistent($event.target.checked)"
|
||||
class="custom-checkbox relative w-10 h-5 appearance-none bg-gray-300 dark:bg-gray-600 rounded-full cursor-pointer transition-colors duration-300 ease-in-out checked:bg-[var(--primary-color)] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[var(--primary-color)] focus:ring-offset-[var(--background-color)]">
|
||||
</div>
|
||||
|
||||
<!-- Animation Speed Setting -->
|
||||
<div class="setting-item mt-6 md:col-span-2"> <!-- md:col-span-2 to make it take full width on medium screens if the grid has 2 columns -->
|
||||
<label class="block text-sm font-medium mb-1 text-[var(--text-color)]">Animation Speed</label>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mb-2">
|
||||
Adjust the speed of screen transitions (applied if animations are enabled).
|
||||
</p>
|
||||
<div class="flex flex-col space-y-1 sm:flex-row sm:space-y-0 sm:space-x-4">
|
||||
<label v-for="option in speedOptions" :key="option.value"
|
||||
class="flex items-center p-2 rounded-md hover:bg-gray-200/50 dark:hover:bg-gray-700/50 cursor-pointer transition-colors duration-150 ease-in-out border border-gray-300 dark:border-gray-600 hover:border-[var(--primary-color)]">
|
||||
<input type="radio"
|
||||
name="transitionSpeed"
|
||||
:value="option.value"
|
||||
:checked="ui.transitionSpeed === option.value"
|
||||
@change="ui.setTransitionSpeed(option.value)"
|
||||
class="form-radio h-4 w-4 text-[var(--primary-color)] focus:ring-1 focus:ring-[var(--primary-color)] border-gray-300 dark:border-gray-500 bg-white dark:bg-gray-800 focus:ring-offset-white dark:focus:ring-offset-gray-900">
|
||||
<span class="ml-2 text-sm text-[var(--text-color)]">{{ option.label }} <span class="text-xs text-gray-500 dark:text-gray-400">({{ option.value }}x)</span></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -70,10 +95,65 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Chat Interface Colors Section -->
|
||||
<section class="mb-10">
|
||||
<h2 class="text-2xl font-semibold mb-6 text-[var(--primary-color)]">Chat Interface Colors</h2>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
|
||||
<div class="setting-item">
|
||||
<label for="chatAgentMessageColor" class="block text-sm font-medium mb-1">Agent Message Color</label>
|
||||
<input type="color" id="chatAgentMessageColor" v-model="ui.chatAgentMessageColor" @input="ui.setChatAgentMessageColor($event.target.value)"
|
||||
class="w-full h-12 p-1 border rounded-lg cursor-pointer shadow-sm hover:opacity-80 transition-opacity border-[var(--secondary-color)] focus:border-[var(--primary-color)] focus:ring-1 focus:ring-[var(--primary-color)]">
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<label for="chatOwnMessageColor" class="block text-sm font-medium mb-1">Own Message Color</label>
|
||||
<input type="color" id="chatOwnMessageColor" v-model="ui.chatOwnMessageColor" @input="ui.setChatOwnMessageColor($event.target.value)"
|
||||
class="w-full h-12 p-1 border rounded-lg cursor-pointer shadow-sm hover:opacity-80 transition-opacity border-[var(--secondary-color)] focus:border-[var(--primary-color)] focus:ring-1 focus:ring-[var(--primary-color)]">
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<label for="chatInputBoxColor" class="block text-sm font-medium mb-1">Input Box Color</label>
|
||||
<input type="color" id="chatInputBoxColor" v-model="ui.chatInputBoxColor" @input="ui.setChatInputBoxColor($event.target.value)"
|
||||
class="w-full h-12 p-1 border rounded-lg cursor-pointer shadow-sm hover:opacity-80 transition-opacity border-[var(--secondary-color)] focus:border-[var(--primary-color)] focus:ring-1 focus:ring-[var(--primary-color)]">
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<label for="chatAccentColor" class="block text-sm font-medium mb-1">Accent Color</label>
|
||||
<input type="color" id="chatAccentColor" v-model="ui.chatAccentColor" @input="ui.setChatAccentColor($event.target.value)"
|
||||
class="w-full h-12 p-1 border rounded-lg cursor-pointer shadow-sm hover:opacity-80 transition-opacity border-[var(--secondary-color)] focus:border-[var(--primary-color)] focus:ring-1 focus:ring-[var(--primary-color)]">
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<label for="chatBackgroundColor" class="block text-sm font-medium mb-1">Background Color</label>
|
||||
<input type="color" id="chatBackgroundColor" v-model="ui.chatBackgroundColor" @input="ui.setChatBackgroundColor($event.target.value)"
|
||||
class="w-full h-12 p-1 border rounded-lg cursor-pointer shadow-sm hover:opacity-80 transition-opacity border-[var(--secondary-color)] focus:border-[var(--primary-color)] focus:ring-1 focus:ring-[var(--primary-color)]">
|
||||
</div>
|
||||
|
||||
<!-- Chat Font Color -->
|
||||
<div class="setting-item">
|
||||
<label for="chatFontColor" class="block text-sm font-medium mb-1">Chat Font Color</label>
|
||||
<input type="color" id="chatFontColor" v-model="ui.chatFontColor" @input="ui.setChatFontColor($event.target.value)"
|
||||
class="w-full h-12 p-1 border rounded-lg cursor-pointer shadow-sm hover:opacity-80 transition-opacity border-[var(--secondary-color)] focus:border-[var(--primary-color)] focus:ring-1 focus:ring-[var(--primary-color)]">
|
||||
</div>
|
||||
|
||||
<!-- Chat Font Family -->
|
||||
<div class="setting-item">
|
||||
<label for="chatFontFamily" class="block text-sm font-medium mb-1">Chat Font Family</label>
|
||||
<input type="text" id="chatFontFamily" v-model="ui.chatFontFamily" @input="ui.setChatFontFamily($event.target.value)"
|
||||
class="w-full p-3 border rounded-lg shadow-sm focus:ring-[var(--primary-color)] focus:border-[var(--primary-color)] transition-all duration-150 ease-in-out bg-white/10 dark:bg-black/10 border-[var(--secondary-color)] hover:border-[var(--primary-color)]"
|
||||
placeholder="e.g., Arial, sans-serif">
|
||||
</div>
|
||||
|
||||
<!-- Chat Font Size -->
|
||||
<div class="setting-item">
|
||||
<label for="chatFontSize" class="block text-sm font-medium mb-1">Chat Font Size (px)</label>
|
||||
<input type="number" id="chatFontSize" v-model.number="ui.chatFontSize" @input="ui.setChatFontSize(Number($event.target.value))"
|
||||
class="w-full p-3 border rounded-lg shadow-sm focus:ring-[var(--primary-color)] focus:border-[var(--primary-color)] transition-all duration-150 ease-in-out bg-white/10 dark:bg-black/10 border-[var(--secondary-color)] hover:border-[var(--primary-color)]"
|
||||
min="8" max="32" step="1">
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Per-Module Color Settings -->
|
||||
<section class="mb-10 module-settings-group">
|
||||
<h3 class="text-xl font-semibold mb-4 text-[var(--primary-color)] border-b border-[var(--secondary-color)] pb-2">Empleados Module</h3>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
|
||||
<div class="setting-item">
|
||||
<label for="accentColorEmpleados" class="block text-sm font-medium mb-1">Accent Color</label>
|
||||
<input type="color" id="accentColorEmpleados" v-model="ui.accentColorEmpleados" @input="ui.setAccentColorEmpleados($event.target.value)"
|
||||
@@ -84,12 +164,20 @@
|
||||
<input type="color" id="tableBgColorEmpleados" v-model="ui.tableBgColorEmpleados" @input="ui.setTableBgColorEmpleados($event.target.value)"
|
||||
class="w-full h-12 p-1 border rounded-lg cursor-pointer shadow-sm hover:opacity-80 transition-opacity border-[var(--secondary-color)] focus:border-[var(--primary-color)] focus:ring-1 focus:ring-[var(--primary-color)]">
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<label for="defaultViewEmpleados" class="block text-sm font-medium mb-1">Default View</label>
|
||||
<select id="defaultViewEmpleados" v-model="ui.defaultViewEmpleados" @change="ui.setDefaultViewEmpleados($event.target.value)"
|
||||
class="w-full p-3 border rounded-lg shadow-sm focus:ring-[var(--primary-color)] focus:border-[var(--primary-color)] transition-all duration-150 ease-in-out bg-white/10 dark:bg-black/10 border-[var(--secondary-color)] hover:border-[var(--primary-color)]">
|
||||
<option value="table">Table</option>
|
||||
<option value="card">Card</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mb-10 module-settings-group">
|
||||
<h3 class="text-xl font-semibold mb-4 text-[var(--primary-color)] border-b border-[var(--secondary-color)] pb-2">Tareas Module</h3>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
|
||||
<div class="setting-item">
|
||||
<label for="accentColorTareas" class="block text-sm font-medium mb-1">Accent Color</label>
|
||||
<input type="color" id="accentColorTareas" v-model="ui.accentColorTareas" @input="ui.setAccentColorTareas($event.target.value)"
|
||||
@@ -100,12 +188,20 @@
|
||||
<input type="color" id="tableBgColorTareas" v-model="ui.tableBgColorTareas" @input="ui.setTableBgColorTareas($event.target.value)"
|
||||
class="w-full h-12 p-1 border rounded-lg cursor-pointer shadow-sm hover:opacity-80 transition-opacity border-[var(--secondary-color)] focus:border-[var(--primary-color)] focus:ring-1 focus:ring-[var(--primary-color)]">
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<label for="defaultViewTareas" class="block text-sm font-medium mb-1">Default View</label>
|
||||
<select id="defaultViewTareas" v-model="ui.defaultViewTareas" @change="ui.setDefaultViewTareas($event.target.value)"
|
||||
class="w-full p-3 border rounded-lg shadow-sm focus:ring-[var(--primary-color)] focus:border-[var(--primary-color)] transition-all duration-150 ease-in-out bg-white/10 dark:bg-black/10 border-[var(--secondary-color)] hover:border-[var(--primary-color)]">
|
||||
<option value="table">Table</option>
|
||||
<option value="card">Card</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mb-10 module-settings-group">
|
||||
<h3 class="text-xl font-semibold mb-4 text-[var(--primary-color)] border-b border-[var(--secondary-color)] pb-2">Planillas Module</h3>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
|
||||
<div class="setting-item">
|
||||
<label for="accentColorPlanillas" class="block text-sm font-medium mb-1">Accent Color</label>
|
||||
<input type="color" id="accentColorPlanillas" v-model="ui.accentColorPlanillas" @input="ui.setAccentColorPlanillas($event.target.value)"
|
||||
@@ -116,12 +212,20 @@
|
||||
<input type="color" id="tableBgColorPlanillas" v-model="ui.tableBgColorPlanillas" @input="ui.setTableBgColorPlanillas($event.target.value)"
|
||||
class="w-full h-12 p-1 border rounded-lg cursor-pointer shadow-sm hover:opacity-80 transition-opacity border-[var(--secondary-color)] focus:border-[var(--primary-color)] focus:ring-1 focus:ring-[var(--primary-color)]">
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<label for="defaultViewPlanillas" class="block text-sm font-medium mb-1">Default View</label>
|
||||
<select id="defaultViewPlanillas" v-model="ui.defaultViewPlanillas" @change="ui.setDefaultViewPlanillas($event.target.value)"
|
||||
class="w-full p-3 border rounded-lg shadow-sm focus:ring-[var(--primary-color)] focus:border-[var(--primary-color)] transition-all duration-150 ease-in-out bg-white/10 dark:bg-black/10 border-[var(--secondary-color)] hover:border-[var(--primary-color)]">
|
||||
<option value="table">Table</option>
|
||||
<option value="card">Card</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mb-10 module-settings-group">
|
||||
<h3 class="text-xl font-semibold mb-4 text-[var(--primary-color)] border-b border-[var(--secondary-color)] pb-2">Asistencias Module</h3>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
|
||||
<div class="setting-item">
|
||||
<label for="accentColorAsistencias" class="block text-sm font-medium mb-1">Accent Color</label>
|
||||
<input type="color" id="accentColorAsistencias" v-model="ui.accentColorAsistencias" @input="ui.setAccentColorAsistencias($event.target.value)"
|
||||
@@ -132,12 +236,20 @@
|
||||
<input type="color" id="tableBgColorAsistencias" v-model="ui.tableBgColorAsistencias" @input="ui.setTableBgColorAsistencias($event.target.value)"
|
||||
class="w-full h-12 p-1 border rounded-lg cursor-pointer shadow-sm hover:opacity-80 transition-opacity border-[var(--secondary-color)] focus:border-[var(--primary-color)] focus:ring-1 focus:ring-[var(--primary-color)]">
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<label for="defaultViewAsistencias" class="block text-sm font-medium mb-1">Default View</label>
|
||||
<select id="defaultViewAsistencias" v-model="ui.defaultViewAsistencias" @change="ui.setDefaultViewAsistencias($event.target.value)"
|
||||
class="w-full p-3 border rounded-lg shadow-sm focus:ring-[var(--primary-color)] focus:border-[var(--primary-color)] transition-all duration-150 ease-in-out bg-white/10 dark:bg-black/10 border-[var(--secondary-color)] hover:border-[var(--primary-color)]">
|
||||
<option value="table">Table</option>
|
||||
<option value="card">Card</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mb-10 module-settings-group">
|
||||
<h3 class="text-xl font-semibold mb-4 text-[var(--primary-color)] border-b border-[var(--secondary-color)] pb-2">Configuración Module</h3>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
|
||||
<div class="setting-item">
|
||||
<label for="accentColorConfiguracion" class="block text-sm font-medium mb-1">Accent Color</label>
|
||||
<input type="color" id="accentColorConfiguracion" v-model="ui.accentColorConfiguracion" @input="ui.setAccentColorConfiguracion($event.target.value)"
|
||||
@@ -148,6 +260,14 @@
|
||||
<input type="color" id="tableBgColorConfiguracion" v-model="ui.tableBgColorConfiguracion" @input="ui.setTableBgColorConfiguracion($event.target.value)"
|
||||
class="w-full h-12 p-1 border rounded-lg cursor-pointer shadow-sm hover:opacity-80 transition-opacity border-[var(--secondary-color)] focus:border-[var(--primary-color)] focus:ring-1 focus:ring-[var(--primary-color)]">
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<label for="defaultViewConfiguracion" class="block text-sm font-medium mb-1">Default View</label>
|
||||
<select id="defaultViewConfiguracion" v-model="ui.defaultViewConfiguracion" @change="ui.setDefaultViewConfiguracion($event.target.value)"
|
||||
class="w-full p-3 border rounded-lg shadow-sm focus:ring-[var(--primary-color)] focus:border-[var(--primary-color)] transition-all duration-150 ease-in-out bg-white/10 dark:bg-black/10 border-[var(--secondary-color)] hover:border-[var(--primary-color)]">
|
||||
<option value="table">Table</option>
|
||||
<option value="card">Card</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -166,6 +286,12 @@ onMounted(() => {
|
||||
isMounted.value = true
|
||||
}, 50)
|
||||
})
|
||||
|
||||
const speedOptions = [
|
||||
{ label: 'Slow', value: 2 },
|
||||
{ label: 'Normal', value: 1 },
|
||||
{ label: 'Fast', value: 0.5 },
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -4,89 +4,68 @@ import { createPinia, setActivePinia } from 'pinia'
|
||||
import { useUi } from '../../stores/useUi' // Adjust path
|
||||
import SettingsView from '../SettingsView.vue' // Adjust path
|
||||
|
||||
// Helper to create a fresh store for each test or group
|
||||
const getFreshStore = () => {
|
||||
setActivePinia(createPinia())
|
||||
return useUi()
|
||||
}
|
||||
// No global 'store' variable here, it will be test-specific or wrapper-specific
|
||||
// Helper to manage store and wrapper creation for tests
|
||||
const setupTestEnvironment = (initialStoreState = {}) => {
|
||||
setActivePinia(createPinia()); // Ensures a fresh Pinia instance
|
||||
|
||||
// Mock localStorage for the store if not already globally mocked by Vitest setup
|
||||
// Ensure this mock is consistent with how useUi.js uses localStorage
|
||||
const localStorageMock = (() => {
|
||||
let lsStore = { ...initialStoreState }; // Initialize with initialStoreState for loading
|
||||
return {
|
||||
getItem: vi.fn((key) => {
|
||||
const value = lsStore[key];
|
||||
// Pinia/useUi expects JSON string or null
|
||||
return typeof value === 'object' ? JSON.stringify(value) : value;
|
||||
}),
|
||||
setItem: vi.fn((key, value) => {
|
||||
// Store as string, as localStorage does
|
||||
lsStore[key] = value.toString();
|
||||
// Make actual saved string available for inspection if needed
|
||||
// This helps verify what _saveAppearanceState would do
|
||||
if (key === 'appearanceSettings') {
|
||||
lsStore['_raw_appearanceSettings'] = value.toString();
|
||||
}
|
||||
}),
|
||||
clear: vi.fn(() => { lsStore = {}; }),
|
||||
removeItem: vi.fn((key) => { delete lsStore[key]; }),
|
||||
// Helper to get the raw string for assertion
|
||||
getRawItem: (key) => lsStore[key]
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(window, 'localStorage', { value: localStorageMock, writable: true });
|
||||
|
||||
// If initialStoreState is provided, simulate it being in localStorage
|
||||
// The useUi store reads from localStorage upon creation.
|
||||
if (Object.keys(initialStoreState).length > 0) {
|
||||
localStorage.setItem('appearanceSettings', JSON.stringify(initialStoreState));
|
||||
} else {
|
||||
localStorage.removeItem('appearanceSettings'); // Ensure no settings if none provided
|
||||
}
|
||||
|
||||
const store = useUi(); // This will now load from the mocked localStorage
|
||||
|
||||
const wrapper = mount(SettingsView, {
|
||||
global: {
|
||||
// Pinia store is automatically available due to setActivePinia
|
||||
},
|
||||
});
|
||||
|
||||
return { wrapper, store, localStorageMock };
|
||||
};
|
||||
|
||||
|
||||
describe('SettingsView.vue', () => {
|
||||
// No global 'store' variable here, it will be test-specific or wrapper-specific
|
||||
|
||||
beforeEach(() => {
|
||||
// Ensure a fresh Pinia instance is active for each test.
|
||||
setActivePinia(createPinia())
|
||||
|
||||
// Clear localStorage mock before each test
|
||||
// Note: The mock itself is defined globally in this file or via setupFiles in Vitest config.
|
||||
// Re-stubbing it or ensuring its state is clean.
|
||||
const localStorageMock = window.localStorage; // Assuming it's already stubbed globally by Vitest setup or top of file
|
||||
localStorageMock.clear();
|
||||
if (localStorageMock.setItem.mockClear) { // vi.fn() specific
|
||||
localStorageMock.setItem.mockClear();
|
||||
localStorageMock.getItem.mockClear();
|
||||
}
|
||||
})
|
||||
|
||||
// createWrapper now also handles store creation and returns the store for spying if needed
|
||||
const createWrapperAndStore = (initialStoreState = {}) => {
|
||||
if (Object.keys(initialStoreState).length > 0) {
|
||||
localStorage.setItem('appearanceSettings', JSON.stringify(initialStoreState));
|
||||
}
|
||||
|
||||
const currentStore = useUi(); // Store is created AFTER localStorage is set for this test
|
||||
|
||||
const wrapper = mount(SettingsView, {
|
||||
global: {
|
||||
// Pinia is active via setActivePinia, component should pick it up
|
||||
},
|
||||
});
|
||||
return { wrapper, store: currentStore }; // Return store for spying
|
||||
}
|
||||
// Vitest's vi.clearAllMocks() might be useful here if mocks persist unexpectedly
|
||||
// This setupTestEnvironment function already handles fresh store and localStorage mock per call.
|
||||
// If window.localStorage was stubbed globally (e.g. in vitest.setup.js), ensure it's reset.
|
||||
// For this example, setupTestEnvironment manages its own localStorage mock.
|
||||
});
|
||||
|
||||
it('renders all input elements with initial values from store', () => {
|
||||
const { wrapper } = createWrapperAndStore({ // Store is created inside here after LS mock
|
||||
let store
|
||||
|
||||
beforeEach(() => {
|
||||
// Create a new Pinia instance and activate it for each test
|
||||
// This also resets the store state for each test
|
||||
store = getFreshStore()
|
||||
|
||||
// Mock localStorage for the store
|
||||
const localStorageMock = (() => {
|
||||
let lsStore = {}
|
||||
return {
|
||||
getItem: vi.fn((key) => lsStore[key] || null),
|
||||
setItem: vi.fn((key, value) => { lsStore[key] = value.toString() }),
|
||||
clear: vi.fn(() => { lsStore = {} }),
|
||||
removeItem: vi.fn((key) => { delete lsStore[key] }),
|
||||
}
|
||||
})()
|
||||
Object.defineProperty(window, 'localStorage', { value: localStorageMock, writable: true })
|
||||
localStorageMock.clear()
|
||||
})
|
||||
|
||||
const createWrapper = (initialStoreState = {}) => {
|
||||
// Apply initial state to the store if provided
|
||||
// This is a bit of a workaround as direct state mutation isn't ideal,
|
||||
// but for testing initial binding it can be simpler than calling actions.
|
||||
// Alternatively, set up localStorage then init store.
|
||||
if (Object.keys(initialStoreState).length > 0) {
|
||||
localStorage.setItem('appearanceSettings', JSON.stringify(initialStoreState))
|
||||
}
|
||||
// Re-initialize store to pick up mocked localStorage if initialStoreState was set
|
||||
store = getFreshStore()
|
||||
|
||||
return mount(SettingsView, {
|
||||
global: {
|
||||
// plugins: [store.$pinia], // Removed: setActivePinia should make it available
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
it('renders all input elements with initial values from store', () => {
|
||||
const wrapper = createWrapper({
|
||||
const { wrapper } = setupTestEnvironment({
|
||||
primaryColor: '#111111',
|
||||
secondaryColor: '#222222',
|
||||
warningColor: '#333333',
|
||||
@@ -96,148 +75,181 @@ describe('SettingsView.vue', () => {
|
||||
animationsEnabled: false,
|
||||
theme: 'dark',
|
||||
// Add new accent colors for comprehensive initial state testing
|
||||
primaryColor: '#111111',
|
||||
secondaryColor: '#222222',
|
||||
warningColor: '#333333',
|
||||
backgroundColor: '#444444',
|
||||
fontFamily: 'Arial',
|
||||
fontSize: 18,
|
||||
animationsEnabled: false,
|
||||
theme: 'dark',
|
||||
accentColorEmpleados: '#123456',
|
||||
accentColorTareas: '#654321',
|
||||
accentColorPlanillas: '#abcdef',
|
||||
accentColorAsistencias: '#fedcba',
|
||||
})
|
||||
accentColorConfiguracion: '#aabbcc',
|
||||
defaultViewEmpleados: 'card', // New default view
|
||||
defaultViewTareas: 'table',
|
||||
defaultViewPlanillas: 'card',
|
||||
defaultViewAsistencias: 'table',
|
||||
defaultViewConfiguracion: 'card',
|
||||
});
|
||||
|
||||
// Check general appearance settings
|
||||
})
|
||||
|
||||
expect(wrapper.find('input#primaryColor').element.value).toBe('#111111')
|
||||
expect(wrapper.find('input#secondaryColor').element.value).toBe('#222222')
|
||||
expect(wrapper.find('input#primaryColor').element.value).toBe('#111111');
|
||||
expect(wrapper.find('input#secondaryColor').element.value).toBe('#222222');
|
||||
expect(wrapper.find('input#warningColor').element.value).toBe('#333333')
|
||||
expect(wrapper.find('input#backgroundColor').element.value).toBe('#444444')
|
||||
expect(wrapper.find('input#fontFamily').element.value).toBe('Arial')
|
||||
expect(wrapper.find('input#fontSize').element.value).toBe('18')
|
||||
expect(wrapper.find('input#animationsEnabled').element.checked).toBe(false)
|
||||
expect(wrapper.find('select#theme').element.value).toBe('dark')
|
||||
expect(wrapper.find('input#animationsEnabled').element.checked).toBe(false);
|
||||
expect(wrapper.find('select#theme').element.value).toBe('dark');
|
||||
|
||||
// Check new module accent color pickers
|
||||
const h2Elements = wrapper.findAll('h2')
|
||||
const sectionTitles = h2Elements.map(h => h.text())
|
||||
expect(sectionTitles).toContain('Module Accent Colors')
|
||||
expect(wrapper.find('input#accentColorEmpleados').element.value).toBe('#123456')
|
||||
expect(wrapper.find('input#accentColorTareas').element.value).toBe('#654321')
|
||||
expect(wrapper.find('input#accentColorPlanillas').element.value).toBe('#abcdef')
|
||||
expect(wrapper.find('input#accentColorAsistencias').element.value).toBe('#fedcba')
|
||||
})
|
||||
// Check module accent color pickers
|
||||
expect(wrapper.find('input#accentColorEmpleados').element.value).toBe('#123456');
|
||||
expect(wrapper.find('input#accentColorTareas').element.value).toBe('#654321');
|
||||
expect(wrapper.find('input#accentColorPlanillas').element.value).toBe('#abcdef');
|
||||
expect(wrapper.find('input#accentColorAsistencias').element.value).toBe('#fedcba');
|
||||
expect(wrapper.find('input#accentColorConfiguracion').element.value).toBe('#aabbcc');
|
||||
|
||||
// Check new default view select elements
|
||||
expect(wrapper.find('select#defaultViewEmpleados').element.value).toBe('card');
|
||||
expect(wrapper.find('select#defaultViewTareas').element.value).toBe('table');
|
||||
expect(wrapper.find('select#defaultViewPlanillas').element.value).toBe('card');
|
||||
expect(wrapper.find('select#defaultViewAsistencias').element.value).toBe('table');
|
||||
expect(wrapper.find('select#defaultViewConfiguracion').element.value).toBe('card');
|
||||
});
|
||||
|
||||
it('calls setPrimaryColor action when primary color input changes', async () => {
|
||||
const { wrapper, store } = createWrapperAndStore()
|
||||
const spy = vi.spyOn(store, 'setPrimaryColor')
|
||||
const colorInput = wrapper.find('input#primaryColor')
|
||||
|
||||
colorInput.element.value = '#FF00FF'
|
||||
await colorInput.trigger('input')
|
||||
|
||||
expect(spy).toHaveBeenCalledWith('#ff00ff')
|
||||
})
|
||||
const { wrapper, store } = setupTestEnvironment();
|
||||
const spy = vi.spyOn(store, 'setPrimaryColor');
|
||||
const colorInput = wrapper.find('input#primaryColor');
|
||||
colorInput.element.value = '#FF00FF';
|
||||
await colorInput.trigger('input');
|
||||
expect(spy).toHaveBeenCalledWith('#ff00ff');
|
||||
});
|
||||
|
||||
it('calls setFontFamily action when font family input changes', async () => {
|
||||
const { wrapper, store } = createWrapperAndStore()
|
||||
const spy = vi.spyOn(store, 'setFontFamily')
|
||||
const input = wrapper.find('input#fontFamily')
|
||||
await input.setValue('Helvetica')
|
||||
})
|
||||
|
||||
it('calls setPrimaryColor action when primary color input changes', async () => {
|
||||
const wrapper = createWrapper()
|
||||
const spy = vi.spyOn(store, 'setPrimaryColor')
|
||||
const colorInput = wrapper.find('input#primaryColor')
|
||||
|
||||
// Simulate color picker actually setting the value and then dispatching input
|
||||
// For input type=color, setting .value and then .trigger('input') is typical
|
||||
colorInput.element.value = '#FF00FF'
|
||||
await colorInput.trigger('input')
|
||||
|
||||
expect(spy).toHaveBeenCalledWith('#ff00ff') // Changed to lowercase
|
||||
})
|
||||
|
||||
it('calls setFontFamily action when font family input changes', async () => {
|
||||
const wrapper = createWrapper()
|
||||
const spy = vi.spyOn(store, 'setFontFamily')
|
||||
const input = wrapper.find('input#fontFamily')
|
||||
await input.setValue('Helvetica') // .setValue also triggers 'input'
|
||||
expect(spy).toHaveBeenCalledWith('Helvetica')
|
||||
})
|
||||
const { wrapper, store } = setupTestEnvironment();
|
||||
const spy = vi.spyOn(store, 'setFontFamily');
|
||||
const input = wrapper.find('input#fontFamily');
|
||||
await input.setValue('Helvetica');
|
||||
expect(spy).toHaveBeenCalledWith('Helvetica');
|
||||
});
|
||||
|
||||
it('calls setFontSize action when font size input changes', async () => {
|
||||
const { wrapper, store } = createWrapperAndStore()
|
||||
const wrapper = createWrapper()
|
||||
const spy = vi.spyOn(store, 'setFontSize')
|
||||
const input = wrapper.find('input#fontSize')
|
||||
await input.setValue('22')
|
||||
expect(spy).toHaveBeenCalledWith(22)
|
||||
})
|
||||
const { wrapper, store } = setupTestEnvironment();
|
||||
const spy = vi.spyOn(store, 'setFontSize');
|
||||
const input = wrapper.find('input#fontSize');
|
||||
await input.setValue('22');
|
||||
expect(spy).toHaveBeenCalledWith(22);
|
||||
});
|
||||
|
||||
it('calls setAnimationsEnabled action when animations checkbox changes', async () => {
|
||||
const { wrapper, store } = createWrapperAndStore({ animationsEnabled: true })
|
||||
const spy = vi.spyOn(store, 'setAnimationsEnabled')
|
||||
const checkbox = wrapper.find('input#animationsEnabled')
|
||||
|
||||
await checkbox.setChecked(false)
|
||||
const wrapper = createWrapper({ animationsEnabled: true }) // Start with true
|
||||
const spy = vi.spyOn(store, 'setAnimationsEnabled')
|
||||
const checkbox = wrapper.find('input#animationsEnabled')
|
||||
|
||||
// For checkboxes, .setValue(false) or .setChecked(false) and then trigger 'change'
|
||||
await checkbox.setChecked(false) // This should trigger the change event for v-model
|
||||
|
||||
expect(spy).toHaveBeenCalledWith(false)
|
||||
})
|
||||
const { wrapper, store } = setupTestEnvironment({ animationsEnabled: true });
|
||||
const spy = vi.spyOn(store, 'setAnimationsEnabled');
|
||||
const checkbox = wrapper.find('input#animationsEnabled');
|
||||
await checkbox.setChecked(false);
|
||||
expect(spy).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('calls setTheme action when theme select changes', async () => {
|
||||
const { wrapper, store } = createWrapperAndStore()
|
||||
const spy = vi.spyOn(store, 'setTheme')
|
||||
const select = wrapper.find('select#theme')
|
||||
await select.setValue('dark')
|
||||
expect(spy).toHaveBeenCalledWith('dark')
|
||||
})
|
||||
const { wrapper, store } = setupTestEnvironment();
|
||||
const spy = vi.spyOn(store, 'setTheme');
|
||||
const select = wrapper.find('select#theme');
|
||||
await select.setValue('dark');
|
||||
expect(spy).toHaveBeenCalledWith('dark');
|
||||
});
|
||||
|
||||
// New tests for module accent color pickers
|
||||
// Tests for module accent color pickers
|
||||
it('calls setAccentColorEmpleados action when empleados accent color input changes', async () => {
|
||||
const { wrapper, store } = createWrapperAndStore()
|
||||
const spy = vi.spyOn(store, 'setAccentColorEmpleados')
|
||||
const colorInput = wrapper.find('input#accentColorEmpleados')
|
||||
colorInput.element.value = '#aabbcc'
|
||||
await colorInput.trigger('input')
|
||||
expect(spy).toHaveBeenCalledWith('#aabbcc')
|
||||
})
|
||||
const { wrapper, store } = setupTestEnvironment();
|
||||
const spy = vi.spyOn(store, 'setAccentColorEmpleados');
|
||||
const colorInput = wrapper.find('input#accentColorEmpleados');
|
||||
colorInput.element.value = '#aabbcc';
|
||||
await colorInput.trigger('input');
|
||||
expect(spy).toHaveBeenCalledWith('#aabbcc');
|
||||
});
|
||||
|
||||
it('calls setAccentColorTareas action when tareas accent color input changes', async () => {
|
||||
const { wrapper, store } = createWrapperAndStore()
|
||||
const spy = vi.spyOn(store, 'setAccentColorTareas')
|
||||
const colorInput = wrapper.find('input#accentColorTareas')
|
||||
colorInput.element.value = '#ccbbaa'
|
||||
await colorInput.trigger('input')
|
||||
expect(spy).toHaveBeenCalledWith('#ccbbaa')
|
||||
})
|
||||
it('calls setAccentColorConfiguracion action when configuracion accent color input changes', async () => {
|
||||
const { wrapper, store } = setupTestEnvironment();
|
||||
const spy = vi.spyOn(store, 'setAccentColorConfiguracion');
|
||||
const colorInput = wrapper.find('input#accentColorConfiguracion');
|
||||
colorInput.element.value = '#ccddee';
|
||||
await colorInput.trigger('input');
|
||||
expect(spy).toHaveBeenCalledWith('#ccddee');
|
||||
});
|
||||
|
||||
it('calls setAccentColorPlanillas action when planillas accent color input changes', async () => {
|
||||
const { wrapper, store } = createWrapperAndStore()
|
||||
const spy = vi.spyOn(store, 'setAccentColorPlanillas')
|
||||
const colorInput = wrapper.find('input#accentColorPlanillas')
|
||||
colorInput.element.value = '#a1b2c3'
|
||||
await colorInput.trigger('input')
|
||||
expect(spy).toHaveBeenCalledWith('#a1b2c3')
|
||||
})
|
||||
|
||||
it('calls setAccentColorAsistencias action when asistencias accent color input changes', async () => {
|
||||
const { wrapper, store } = createWrapperAndStore()
|
||||
const spy = vi.spyOn(store, 'setAccentColorAsistencias')
|
||||
const colorInput = wrapper.find('input#accentColorAsistencias')
|
||||
colorInput.element.value = '#c3b2a1'
|
||||
await colorInput.trigger('input')
|
||||
expect(spy).toHaveBeenCalledWith('#c3b2a1')
|
||||
})
|
||||
// *** ADD NEW TESTS FOR DEFAULT VIEW SELECTS HERE ***
|
||||
describe('Default View Selects', () => {
|
||||
it('renders default view select for Empleados module and calls action on change', async () => {
|
||||
const { wrapper, store } = setupTestEnvironment({ defaultViewEmpleados: 'table' });
|
||||
const select = wrapper.find('select#defaultViewEmpleados');
|
||||
expect(select.exists()).toBe(true);
|
||||
expect(select.element.value).toBe('table');
|
||||
const options = select.findAll('option');
|
||||
expect(options.length).toBe(2);
|
||||
expect(options[0].text()).toBe('Table');
|
||||
expect(options[0].element.value).toBe('table');
|
||||
expect(options[1].text()).toBe('Card');
|
||||
expect(options[1].element.value).toBe('card');
|
||||
|
||||
const spy = vi.spyOn(store, 'setDefaultViewEmpleados');
|
||||
await select.setValue('card');
|
||||
expect(spy).toHaveBeenCalledWith('card');
|
||||
expect(select.element.value).toBe('card'); // Assuming v-model updates from store mock correctly
|
||||
});
|
||||
|
||||
it('renders default view select for Tareas module and calls action on change', async () => {
|
||||
const { wrapper, store } = setupTestEnvironment({ defaultViewTareas: 'table' });
|
||||
const select = wrapper.find('select#defaultViewTareas');
|
||||
expect(select.exists()).toBe(true);
|
||||
expect(select.element.value).toBe('table');
|
||||
const spy = vi.spyOn(store, 'setDefaultViewTareas');
|
||||
await select.setValue('card');
|
||||
expect(spy).toHaveBeenCalledWith('card');
|
||||
expect(select.element.value).toBe('card');
|
||||
});
|
||||
|
||||
it('renders default view select for Planillas module and calls action on change', async () => {
|
||||
const { wrapper, store } = setupTestEnvironment({ defaultViewPlanillas: 'table' });
|
||||
const select = wrapper.find('select#defaultViewPlanillas');
|
||||
expect(select.exists()).toBe(true);
|
||||
expect(select.element.value).toBe('table');
|
||||
const spy = vi.spyOn(store, 'setDefaultViewPlanillas');
|
||||
await select.setValue('card');
|
||||
expect(spy).toHaveBeenCalledWith('card');
|
||||
expect(select.element.value).toBe('card');
|
||||
});
|
||||
|
||||
it('renders default view select for Asistencias module and calls action on change', async () => {
|
||||
const { wrapper, store } = setupTestEnvironment({ defaultViewAsistencias: 'table' });
|
||||
const select = wrapper.find('select#defaultViewAsistencias');
|
||||
expect(select.exists()).toBe(true);
|
||||
expect(select.element.value).toBe('table');
|
||||
const spy = vi.spyOn(store, 'setDefaultViewAsistencias');
|
||||
await select.setValue('card');
|
||||
expect(spy).toHaveBeenCalledWith('card');
|
||||
expect(select.element.value).toBe('card');
|
||||
});
|
||||
|
||||
it('renders default view select for Configuracion module and calls action on change', async () => {
|
||||
const { wrapper, store } = setupTestEnvironment({ defaultViewConfiguracion: 'table' });
|
||||
const select = wrapper.find('select#defaultViewConfiguracion');
|
||||
expect(select.exists()).toBe(true);
|
||||
expect(select.element.value).toBe('table');
|
||||
const spy = vi.spyOn(store, 'setDefaultViewConfiguracion');
|
||||
await select.setValue('card');
|
||||
expect(spy).toHaveBeenCalledWith('card');
|
||||
expect(select.element.value).toBe('card');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('updates input values when store state changes programmatically', async () => {
|
||||
const { wrapper, store } = createWrapperAndStore() // Store instance for this test
|
||||
const { wrapper, store } = setupTestEnvironment();
|
||||
|
||||
// Test primaryColor
|
||||
store.primaryColor = '#001122' // Directly manipulate the store used by the component
|
||||
store.primaryColor = '#001122';
|
||||
=======
|
||||
const wrapper = createWrapper()
|
||||
const spy = vi.spyOn(store, 'setTheme')
|
||||
@@ -251,51 +263,43 @@ describe('SettingsView.vue', () => {
|
||||
|
||||
// Test primaryColor
|
||||
store.primaryColor = '#001122'
|
||||
await wrapper.vm.$nextTick() // Wait for Vue to react to state change
|
||||
expect(wrapper.find('input#primaryColor').element.value).toBe('#001122')
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.find('input#primaryColor').element.value).toBe('#001122');
|
||||
|
||||
// Test fontFamily
|
||||
store.fontFamily = 'Verdana'
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.find('input#fontFamily').element.value).toBe('Verdana')
|
||||
store.fontFamily = 'Verdana';
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.find('input#fontFamily').element.value).toBe('Verdana');
|
||||
|
||||
// Test fontSize
|
||||
store.fontSize = 12
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.find('input#fontSize').element.value).toBe('12')
|
||||
store.fontSize = 12;
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.find('input#fontSize').element.value).toBe('12');
|
||||
|
||||
// Test animationsEnabled
|
||||
store.animationsEnabled = false
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.find('input#animationsEnabled').element.checked).toBe(false)
|
||||
store.animationsEnabled = false;
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.find('input#animationsEnabled').element.checked).toBe(false);
|
||||
|
||||
// Test theme
|
||||
store.theme = 'dark'
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.find('select#theme').element.value).toBe('dark')
|
||||
store.theme = 'dark';
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.find('select#theme').element.value).toBe('dark');
|
||||
|
||||
// Test one of the new accent colors
|
||||
store.accentColorEmpleados = '#998877'
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.find('input#accentColorEmpleados').element.value).toBe('#998877')
|
||||
})
|
||||
store.accentColorEmpleados = '#998877';
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.find('input#accentColorEmpleados').element.value).toBe('#998877');
|
||||
|
||||
// Test one of the new default view selects
|
||||
store.defaultViewEmpleados = 'card';
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.find('select#defaultViewEmpleados').element.value).toBe('card');
|
||||
});
|
||||
|
||||
// Test for the initial fade-in animation - checking class
|
||||
it('applies opacity transition class after mount', async () => {
|
||||
// Mock setTimeout to control its execution
|
||||
vi.useFakeTimers()
|
||||
vi.useFakeTimers();
|
||||
const { wrapper } = setupTestEnvironment();
|
||||
expect(wrapper.find('.settings-view').classes()).not.toContain('opacity-100');
|
||||
|
||||
const { wrapper } = createWrapperAndStore() // Use the new function name
|
||||
const wrapper = createWrapper()
|
||||
expect(wrapper.find('.settings-view').classes()).not.toContain('opacity-100')
|
||||
|
||||
// Advance timers by the amount used in setTimeout in SettingsView.vue (50ms)
|
||||
vi.advanceTimersByTime(100) // Advance a bit more to be sure
|
||||
await wrapper.vm.$nextTick() // Allow Vue to re-render
|
||||
|
||||
expect(wrapper.find('.settings-view').classes()).toContain('opacity-100')
|
||||
|
||||
vi.useRealTimers() // Restore real timers
|
||||
})
|
||||
|
||||
})
|
||||
vi.advanceTimersByTime(100);
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.find('.settings-view').classes()).toContain('opacity-100');
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="asistencia-form-container">
|
||||
<div class="asistencia-form-container" :style="{ backgroundColor: uiStore.tableBgColorAsistencias }">
|
||||
<h2>{{ formTitle }}</h2>
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<div class="form-group">
|
||||
@@ -48,6 +48,7 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, onUnmounted, computed, watch } from 'vue';
|
||||
import { useAsistenciasStore } from '../../stores/useAsistencias';
|
||||
import { useUi } from '@/stores/useUi'; // Corrected UI store import
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
|
||||
const props = defineProps({
|
||||
@@ -57,6 +58,7 @@ const props = defineProps({
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const asistenciasStore = useAsistenciasStore();
|
||||
const uiStore = useUi(); // Corrected UI store instantiation
|
||||
|
||||
const formatDateTimeForInput = (dateString) => {
|
||||
const date = dateString ? new Date(dateString) : new Date();
|
||||
@@ -207,7 +209,7 @@ const handleCancel = () => {
|
||||
max-width: 600px;
|
||||
margin: 20px auto;
|
||||
padding: 25px;
|
||||
background-color: #f9f9f9;
|
||||
/* background-color: #f9f9f9; */ /* Removed to use dynamic background */
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
@@ -7,13 +7,23 @@
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div class="mb-6 flex justify-end items-center space-x-3">
|
||||
<span class="text-sm font-medium text-gray-700">Cambiar Vista:</span>
|
||||
<button @click="currentView = 'card'" :class="btnClass('card')">
|
||||
Tarjetas
|
||||
<!-- View Toggle Buttons -->
|
||||
<div class="mb-4 flex justify-end space-x-2">
|
||||
<button
|
||||
@click="currentView = 'table'"
|
||||
:class="btnViewClass('table')"
|
||||
aria-label="Table View"
|
||||
title="Table View"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16" /></svg>
|
||||
</button>
|
||||
<button @click="currentView = 'table'" :class="btnClass('table')">
|
||||
Tabla
|
||||
<button
|
||||
@click="currentView = 'card'"
|
||||
:class="btnViewClass('card')"
|
||||
aria-label="Card View"
|
||||
title="Card View"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -60,18 +70,21 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useAsistenciasStore } from '../../stores/useAsistencias';
|
||||
import { useUi } from '../../stores/useUi'; // Import useUi
|
||||
import { useRouter } from 'vue-router';
|
||||
import TablaAsistencias from '../../components/asistencias/tablaAsistencias.vue';
|
||||
import CardAsistencia from '../../components/asistencias/cardAsistencia.vue';
|
||||
|
||||
const asistenciasStore = useAsistenciasStore();
|
||||
const ui = useUi(); // Access the ui store
|
||||
const router = useRouter();
|
||||
|
||||
const isLoading = ref(true);
|
||||
const errorLoading = ref(false);
|
||||
const errorMessage = ref('');
|
||||
|
||||
const currentView = ref('table'); // Default to table view
|
||||
// Initialize currentView from the store's default setting for asistencias
|
||||
const currentView = ref(ui.defaultViewAsistencias);
|
||||
|
||||
const asistenciasList = computed(() => asistenciasStore.asistencias);
|
||||
|
||||
@@ -100,12 +113,12 @@ const handleEditAsistencia = (asistenciaId) => {
|
||||
router.push({ name: 'asistencias-edit', params: { id: asistenciaId } });
|
||||
};
|
||||
|
||||
const btnClass = (view) => {
|
||||
const baseClasses = 'px-4 py-2 rounded-md text-sm font-medium focus:outline-none focus:ring-2 focus:ring-offset-2';
|
||||
if (currentView.value === view) {
|
||||
return `${baseClasses} text-white shadow-sm view-toggle-active-asistencias`;
|
||||
const btnViewClass = (viewType) => {
|
||||
const base = 'p-2 rounded-md transition-colors duration-150 ease-in-out';
|
||||
if (currentView.value === viewType) {
|
||||
return `${base} bg-[var(--accent-color-asistencias)] text-white shadow-lg`;
|
||||
}
|
||||
return `${baseClasses} bg-gray-200 text-gray-700 hover:bg-gray-300 focus:ring-gray-400`;
|
||||
return `${base} bg-gray-200 text-gray-700 hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600`;
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
216
ui/src/views/asistencias/__tests__/AsistenciasIndex.spec.js
Normal file
216
ui/src/views/asistencias/__tests__/AsistenciasIndex.spec.js
Normal file
@@ -0,0 +1,216 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { useUi } from '@/stores/useUi'
|
||||
import { useAsistenciasStore } from '@/stores/useAsistencias'
|
||||
import AsistenciasIndex from '../AsistenciasIndex.vue'
|
||||
import TablaAsistencias from '@/components/asistencias/tablaAsistencias.vue'
|
||||
import CardAsistencia from '@/components/asistencias/cardAsistencia.vue'
|
||||
|
||||
// Mock child components
|
||||
vi.mock('@/components/asistencias/tablaAsistencias.vue', () => ({
|
||||
default: {
|
||||
name: 'TablaAsistencias',
|
||||
props: ['asistencias', 'isLoading'], // Match actual props if different
|
||||
emits: ['edit'],
|
||||
template: '<div data-testid="tabla-asistencias"></div>',
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/components/asistencias/cardAsistencia.vue', () => ({
|
||||
default: {
|
||||
name: 'CardAsistencia',
|
||||
props: ['asistencia'], // Match actual props
|
||||
emits: ['edit'],
|
||||
template: '<div data-testid="card-asistencia"></div>',
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock stores
|
||||
const mockSetDefaultViewAsistencias = vi.fn();
|
||||
const mockFetchAsistencias = vi.fn();
|
||||
|
||||
vi.mock('@/stores/useUi', () => ({
|
||||
useUi: vi.fn(() => ({
|
||||
defaultViewAsistencias: 'table', // Default mock value
|
||||
setDefaultViewAsistencias: mockSetDefaultViewAsistencias,
|
||||
})),
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/useAsistencias', () => ({
|
||||
useAsistenciasStore: vi.fn(() => ({
|
||||
asistencias: [],
|
||||
fetchAsistencias: mockFetchAsistencias,
|
||||
})),
|
||||
}))
|
||||
|
||||
describe('AsistenciasIndex.vue', () => {
|
||||
let uiStoreMock
|
||||
let asistenciasStoreMock
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
mockFetchAsistencias.mockClear().mockResolvedValue([])
|
||||
mockSetDefaultViewAsistencias.mockClear()
|
||||
|
||||
uiStoreMock = useUi()
|
||||
asistenciasStoreMock = useAsistenciasStore()
|
||||
})
|
||||
|
||||
const mountComponent = () => {
|
||||
return mount(AsistenciasIndex, {
|
||||
global: {
|
||||
// Stubs can be defined here too if not using vi.mock for everything
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
it('fetches asistencias on mount', async () => {
|
||||
mountComponent()
|
||||
// It seems the component has its own isLoading ref, wait for it to resolve
|
||||
await mockFetchAsistencias(); // Ensure the promise resolves
|
||||
expect(mockFetchAsistencias).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
describe('View Rendering based on useUi store', () => {
|
||||
it('renders TablaAsistencias when defaultViewAsistencias is "table"', async () => {
|
||||
uiStoreMock.defaultViewAsistencias = 'table'
|
||||
asistenciasStoreMock.asistencias = [{ id: 1, empleado: 'Test Employee', fecha: '2023-01-01' }]
|
||||
|
||||
const wrapper = mountComponent()
|
||||
// Wait for internal loading state and then data processing
|
||||
await mockFetchAsistencias();
|
||||
await wrapper.vm.$nextTick()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
|
||||
expect(wrapper.findComponent({ name: 'TablaAsistencias' }).exists()).toBe(true)
|
||||
expect(wrapper.findComponent({ name: 'TablaAsistencias' }).props('asistencias')).toEqual(asistenciasStoreMock.asistencias)
|
||||
expect(wrapper.findComponent({ name: 'CardAsistencia' }).exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('renders CardAsistencia when defaultViewAsistencias is "card"', async () => {
|
||||
uiStoreMock.defaultViewAsistencias = 'card'
|
||||
asistenciasStoreMock.asistencias = [{ id: 1, empleado: 'E1' }, { id: 2, empleado: 'E2' }]
|
||||
|
||||
const wrapper = mountComponent()
|
||||
await mockFetchAsistencias();
|
||||
await wrapper.vm.$nextTick()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const cardWrappers = wrapper.findAllComponents({ name: 'CardAsistencia' })
|
||||
expect(cardWrappers.length).toBe(asistenciasStoreMock.asistencias.length)
|
||||
expect(cardWrappers[0].props('asistencia')).toEqual(asistenciasStoreMock.asistencias[0])
|
||||
expect(wrapper.findComponent({ name: 'TablaAsistencias' }).exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('renders no data message for table view when no asistencias exist', async () => {
|
||||
uiStoreMock.defaultViewAsistencias = 'table';
|
||||
asistenciasStoreMock.asistencias = [];
|
||||
|
||||
const wrapper = mountComponent();
|
||||
await mockFetchAsistencias();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.findComponent({ name: 'TablaAsistencias' }).exists()).toBe(true);
|
||||
// The component might show "Cargando asistencias..." initially, then the no data message.
|
||||
// We need to ensure loading is complete.
|
||||
expect(wrapper.text()).toContain('No hay asistencias para mostrar');
|
||||
});
|
||||
|
||||
it('renders no data message for card view when no asistencias exist', async () => {
|
||||
uiStoreMock.defaultViewAsistencias = 'card';
|
||||
asistenciasStoreMock.asistencias = [];
|
||||
|
||||
const wrapper = mountComponent();
|
||||
await mockFetchAsistencias();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.findAllComponents({ name: 'CardAsistencia' }).length).toBe(0);
|
||||
expect(wrapper.text()).toContain('No hay asistencias para mostrar');
|
||||
});
|
||||
})
|
||||
|
||||
describe('Local View Toggle Buttons', () => {
|
||||
it('renders toggle buttons and reflects initial view from store (table)', async () => {
|
||||
uiStoreMock.defaultViewAsistencias = 'table';
|
||||
const wrapper = mountComponent();
|
||||
await mockFetchAsistencias();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const tableViewButton = wrapper.find('button[aria-label="Table View"]');
|
||||
const cardViewButton = wrapper.find('button[aria-label="Card View"]');
|
||||
|
||||
expect(tableViewButton.exists()).toBe(true);
|
||||
expect(cardViewButton.exists()).toBe(true);
|
||||
|
||||
// Check active class based on btnViewClass logic
|
||||
// Active: bg-[var(--accent-color-asistencias)] text-white
|
||||
// Inactive: bg-gray-200 text-gray-700
|
||||
expect(tableViewButton.classes()).toContain('bg-[var(--accent-color-asistencias)]');
|
||||
expect(cardViewButton.classes()).toContain('bg-gray-200');
|
||||
});
|
||||
|
||||
it('renders toggle buttons and reflects initial view from store (card)', async () => {
|
||||
uiStoreMock.defaultViewAsistencias = 'card';
|
||||
const wrapper = mountComponent();
|
||||
await mockFetchAsistencias();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const tableViewButton = wrapper.find('button[aria-label="Table View"]');
|
||||
const cardViewButton = wrapper.find('button[aria-label="Card View"]');
|
||||
|
||||
expect(cardViewButton.classes()).toContain('bg-[var(--accent-color-asistencias)]');
|
||||
expect(tableViewButton.classes()).toContain('bg-gray-200');
|
||||
});
|
||||
|
||||
it('switches to card view on button click and updates button styles, does not call global store action', async () => {
|
||||
uiStoreMock.defaultViewAsistencias = 'table';
|
||||
asistenciasStoreMock.asistencias = [{ id: 1, empleado: 'Test' }];
|
||||
const wrapper = mountComponent();
|
||||
await mockFetchAsistencias();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const cardViewButton = wrapper.find('button[aria-label="Card View"]');
|
||||
const tableViewButton = wrapper.find('button[aria-label="Table View"]');
|
||||
|
||||
await cardViewButton.trigger('click');
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.findComponent({ name: 'CardAsistencia' }).exists()).toBe(true);
|
||||
expect(wrapper.findComponent({ name: 'TablaAsistencias' }).exists()).toBe(false);
|
||||
expect(cardViewButton.classes()).toContain('bg-[var(--accent-color-asistencias)]');
|
||||
expect(tableViewButton.classes()).toContain('bg-gray-200');
|
||||
expect(mockSetDefaultViewAsistencias).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('switches back to table view on button click and updates button styles, does not call global store action', async () => {
|
||||
uiStoreMock.defaultViewAsistencias = 'card'; // Start with card view
|
||||
asistenciasStoreMock.asistencias = [{ id: 1, empleado: 'Test' }];
|
||||
const wrapper = mountComponent();
|
||||
await mockFetchAsistencias();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
// Initially card view is active
|
||||
const cardViewButton = wrapper.find('button[aria-label="Card View"]');
|
||||
const tableViewButton = wrapper.find('button[aria-label="Table View"]');
|
||||
expect(cardViewButton.classes()).toContain('bg-[var(--accent-color-asistencias)]');
|
||||
|
||||
|
||||
await tableViewButton.trigger('click');
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.findComponent({ name: 'TablaAsistencias' }).exists()).toBe(true);
|
||||
expect(wrapper.findComponent({ name: 'CardAsistencia' }).exists()).toBe(false);
|
||||
expect(tableViewButton.classes()).toContain('bg-[var(--accent-color-asistencias)]');
|
||||
expect(cardViewButton.classes()).toContain('bg-gray-200');
|
||||
expect(mockSetDefaultViewAsistencias).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
})
|
||||
@@ -1,12 +1,13 @@
|
||||
<template>
|
||||
<div class="p-6 bg-gray-100 min-h-screen">
|
||||
<div class="p-6 min-h-screen empleado-form-page-container">
|
||||
<h1 class="text-3xl font-bold mb-8 text-center text-gray-700">
|
||||
{{ isEditMode ? 'Editar Empleado' : 'Crear Empleado' }}
|
||||
</h1>
|
||||
|
||||
<form
|
||||
@submit.prevent="handleSubmit"
|
||||
class="max-w-lg mx-auto bg-white p-8 rounded-lg shadow-lg"
|
||||
class="max-w-lg mx-auto p-8 rounded-lg shadow-lg empleado-actual-form"
|
||||
|
||||
>
|
||||
<!-- ───────── Nombre ───────── -->
|
||||
<div class="mb-6">
|
||||
@@ -149,6 +150,7 @@ import { ref, onMounted, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useEmpleadosStore } from '@/stores/useEmpleados.js'
|
||||
import { useUi } from '@/stores/useUi'; // Corrected UI store import
|
||||
|
||||
/* ───── Tipos ───── */
|
||||
interface EmpleadoForm {
|
||||
@@ -179,6 +181,7 @@ const router = useRouter()
|
||||
/* ───── Store ───── */
|
||||
const empleadosStore = useEmpleadosStore()
|
||||
const { currentEmpleado } = storeToRefs(empleadosStore)
|
||||
const uiStore = useUi(); // Corrected UI store instantiation
|
||||
|
||||
/* ───── State ───── */
|
||||
const form = ref<EmpleadoForm>(defaultForm())
|
||||
@@ -239,6 +242,19 @@ input:required:invalid {
|
||||
/* Removing generic input:focus, button:focus as they are too broad */
|
||||
|
||||
/* --- Look & feel extra (opcional, podés ajustar) --- */
|
||||
.empleado-form-page-container {
|
||||
background-color: var(--background-color, #f3f4f6); /* Fallback to a light gray */
|
||||
}
|
||||
|
||||
.empleado-actual-form {
|
||||
background-color: #ffffff; /* Default white for light theme */
|
||||
}
|
||||
|
||||
/* Example for dark theme (assuming a parent class .dark or data attribute) */
|
||||
.dark .empleado-actual-form {
|
||||
background-color: #2d3748; /* A dark gray for dark theme */
|
||||
}
|
||||
|
||||
.form-container { background-color: var(--background-color); } /* Use theme background */
|
||||
.form-card { box-shadow: 0 10px 15px -3px rgba(0,0,0,.1),
|
||||
0 4px 6px -2px rgba(0,0,0,.05); }
|
||||
|
||||
@@ -1,48 +1,40 @@
|
||||
<template>
|
||||
<div class="p-6 bg-gray-50 min-h-screen">
|
||||
<div class="empleados-index-container">
|
||||
<!-- … encabezado … -->
|
||||
<header class="mb-8">
|
||||
<div class="flex justify-between items-center">
|
||||
<h1 class="text-4xl font-bold text-gray-800">Gestión de Empleados</h1>
|
||||
<button
|
||||
@click="goToCreateEmployee"
|
||||
class="create-button px-6 py-3 text-white font-semibold rounded-lg shadow-md focus:outline-none transition duration-150 ease-in-out"
|
||||
>
|
||||
<!-- ícono ➕ -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline-block mr-2" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Crear Empleado
|
||||
</button>
|
||||
</div>
|
||||
<p class="mt-2 text-gray-600">Visualiza, crea y gestiona los empleados de la organización.</p>
|
||||
<header class="page-header">
|
||||
<h1>Gestión de Empleados</h1>
|
||||
<button @click="goToCreateEmployee" class="btn-create">
|
||||
Crear Empleado
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<!-- selector de vista -->
|
||||
<div class="mb-6 flex justify-end items-center space-x-3">
|
||||
<span class="text-sm font-medium text-gray-700">Cambiar Vista:</span>
|
||||
<button
|
||||
@click="currentView = 'card'"
|
||||
:class="btnClass('card')"
|
||||
>
|
||||
Tarjetas
|
||||
</button>
|
||||
<div class="mb-4 flex justify-end space-x-2">
|
||||
<button
|
||||
@click="currentView = 'table'"
|
||||
:class="btnClass('table')"
|
||||
:class="btnViewClass('table')"
|
||||
aria-label="Table View"
|
||||
title="Table View"
|
||||
>
|
||||
Tabla
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16" /></svg>
|
||||
</button>
|
||||
<button
|
||||
@click="currentView = 'card'"
|
||||
:class="btnViewClass('card')"
|
||||
aria-label="Card View"
|
||||
title="Card View"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- contenido -->
|
||||
<div>
|
||||
<div v-if="loading" class="text-center py-10">
|
||||
<p class="text-gray-500 text-xl">Cargando empleados...</p>
|
||||
</div>
|
||||
<div v-if="loading" class="loading-message">Cargando empleados...</div>
|
||||
|
||||
<div v-else-if="error" class="text-center py-10">
|
||||
<p class="text-red-500 text-xl">Error al cargar los empleados: {{ error }}</p>
|
||||
<div v-else-if="error" class="error-message-full">
|
||||
<p>Error al cargar los empleados. Por favor, intente de nuevo más tarde.</p>
|
||||
<p v-if="error">Detalle: {{ error }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
@@ -56,19 +48,16 @@
|
||||
:key="employee.id"
|
||||
:employee="employee"
|
||||
/>
|
||||
<div v-if="employees.length === 0" class="col-span-full text-center py-10">
|
||||
<p class="text-gray-500 text-xl">No hay empleados para mostrar en la vista de tarjetas.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="currentView === 'card' && employees.length === 0 && !loading" class="no-data-message">
|
||||
No hay empleados para mostrar en la vista de tarjetas.
|
||||
</div>
|
||||
|
||||
<!-- vista de tabla -->
|
||||
<div v-if="currentView === 'table'">
|
||||
<TablaEmpleados :employees="employees" />
|
||||
<div
|
||||
v-if="employees.length === 0"
|
||||
class="text-center py-10 bg-white shadow-md rounded-lg mt-4"
|
||||
>
|
||||
<p class="text-gray-500 text-xl">No hay empleados para mostrar en la vista de tabla.</p>
|
||||
<div v-if="employees.length === 0 && !loading" class="no-data-message">
|
||||
No hay empleados para mostrar en la vista de tabla.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -80,6 +69,7 @@
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useUi } from '@/stores/useUi'; // Import useUi
|
||||
|
||||
import CardEmpleado from '@/components/empleados/cardEmpleado.vue';
|
||||
import TablaEmpleados from '@/components/empleados/tablaEmpleados.vue';
|
||||
@@ -87,7 +77,8 @@ import { useEmpleadosStore } from '@/stores/useEmpleados.js'; // ruta según tu
|
||||
|
||||
// --- refs locales ---
|
||||
const router = useRouter();
|
||||
const currentView = ref<'card' | 'table'>('card');
|
||||
const ui = useUi(); // Access the ui store
|
||||
const currentView = ref<'card' | 'table'>(ui.defaultViewEmpleados); // Initialize from store
|
||||
const loading = ref(true);
|
||||
const error = ref<string | null>(null);
|
||||
|
||||
@@ -99,14 +90,13 @@ const { empleados } = storeToRefs(empleadosStore);
|
||||
const employees = empleados;
|
||||
|
||||
// --- helpers ---
|
||||
const btnClass = (view: 'card' | 'table') => {
|
||||
const baseClasses = 'px-4 py-2 rounded-md text-sm font-medium focus:outline-none focus:ring-2 focus:ring-offset-2';
|
||||
if (currentView.value === view) {
|
||||
// Active button uses accent color
|
||||
return `${baseClasses} text-white shadow-sm view-toggle-active`;
|
||||
// Removed btnClass as manual toggle buttons are removed
|
||||
const btnViewClass = (viewType: 'card' | 'table') => {
|
||||
const base = 'p-2 rounded-md transition-colors duration-150 ease-in-out';
|
||||
if (currentView.value === viewType) {
|
||||
return `${base} bg-[var(--accent-color-empleados)] text-white shadow-lg`;
|
||||
}
|
||||
// Inactive button uses secondary/gray styling
|
||||
return `${baseClasses} bg-gray-200 text-gray-700 hover:bg-gray-300 focus:ring-gray-400`;
|
||||
return `${base} bg-gray-200 text-gray-700 hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600`;
|
||||
};
|
||||
|
||||
// --- fetch inicial ---
|
||||
@@ -130,22 +120,84 @@ const goToCreateEmployee = () => router.push({ name: 'empleados-new' });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.min-h-screen { min-height: calc(100vh - var(--navbar-height, 0px)); } /* Assuming --navbar-height is defined elsewhere or adjust */
|
||||
.empleados-index-container {
|
||||
padding: 20px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
font-family: Arial, sans-serif;
|
||||
min-height: calc(100vh - var(--navbar-height, 0px)); /* Assuming --navbar-height is defined elsewhere or adjust */
|
||||
}
|
||||
|
||||
.create-button {
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 25px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
color: var(--accent-color-empleados); /* Accent for title */
|
||||
font-size: 2.2em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-create {
|
||||
background-color: var(--accent-color-empleados);
|
||||
color: white; /* Assuming accent is dark enough */
|
||||
padding: 12px 18px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease-in-out, box-shadow 0.2s ease, filter 0.2s ease;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
.create-button:hover {
|
||||
filter: brightness(1.1);
|
||||
|
||||
.btn-create:hover {
|
||||
filter: brightness(0.9);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
.create-button:focus {
|
||||
box-shadow: 0 0 0 2px var(--background-color), 0 0 0 4px var(--accent-color-empleados);
|
||||
|
||||
.btn-create:focus {
|
||||
box-shadow: 0 0 0 2px var(--background-color, #fff), 0 0 0 4px var(--accent-color-empleados);
|
||||
}
|
||||
|
||||
.loading-message,
|
||||
.error-message-full,
|
||||
.no-data-message {
|
||||
text-align: center;
|
||||
padding: 25px;
|
||||
margin-top: 25px;
|
||||
border-radius: 8px;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.loading-message {
|
||||
color: #7f8c8d; /* Gray */
|
||||
}
|
||||
|
||||
.error-message-full {
|
||||
background-color: #fdedec; /* Lighter red */
|
||||
color: #e74c3c; /* Strong red */
|
||||
border: 1px solid #f5b7b1; /* Light red border */
|
||||
}
|
||||
.error-message-full p {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.no-data-message {
|
||||
background-color: #eafaf1; /* Lighter green/blue */
|
||||
color: #2ecc71; /* Green */
|
||||
border: 1px solid #a3e4d7; /* Light green/blue border */
|
||||
}
|
||||
|
||||
.view-toggle-active {
|
||||
background-color: var(--accent-color-empleados);
|
||||
/* For focus, assuming white text on accent. Adjust if needed. */
|
||||
box-shadow: 0 0 0 2px var(--background-color), 0 0 0 4px var(--accent-color-empleados);
|
||||
box-shadow: 0 0 0 2px var(--background-color, #fff), 0 0 0 4px var(--accent-color-empleados);
|
||||
}
|
||||
/* Inactive toggle button styling is handled by Tailwind classes directly in btnClass function */
|
||||
|
||||
|
||||
208
ui/src/views/empleados/__tests__/EmpleadosIndex.spec.js
Normal file
208
ui/src/views/empleados/__tests__/EmpleadosIndex.spec.js
Normal file
@@ -0,0 +1,208 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { useUi } from '@/stores/useUi'
|
||||
import { useEmpleadosStore } from '@/stores/useEmpleados'
|
||||
import EmpleadosIndex from '../EmpleadosIndex.vue'
|
||||
import TablaEmpleados from '@/components/empleados/tablaEmpleados.vue'
|
||||
import CardEmpleado from '@/components/empleados/cardEmpleado.vue'
|
||||
|
||||
// Mock the child components to simplify testing and focus on EmpleadosIndex logic
|
||||
vi.mock('@/components/empleados/tablaEmpleados.vue', () => ({
|
||||
default: {
|
||||
name: 'TablaEmpleados',
|
||||
props: ['employees'],
|
||||
template: '<div data-testid="tabla-empleados"></div>',
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/components/empleados/cardEmpleado.vue', () => ({
|
||||
default: {
|
||||
name: 'CardEmpleado',
|
||||
props: ['employee'],
|
||||
template: '<div data-testid="card-empleado"></div>',
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock the stores
|
||||
// We need to define the mock functions before they are used by `vi.mock`
|
||||
const mockSetDefaultViewEmpleados = vi.fn();
|
||||
const mockFetchEmpleados = vi.fn();
|
||||
|
||||
vi.mock('@/stores/useUi', () => ({
|
||||
useUi: vi.fn(() => ({
|
||||
defaultViewEmpleados: 'table', // Default mock value
|
||||
// Add any other state or actions needed by the component from useUi
|
||||
setDefaultViewEmpleados: mockSetDefaultViewEmpleados,
|
||||
})),
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/useEmpleados', () => ({
|
||||
useEmpleadosStore: vi.fn(() => ({
|
||||
empleados: [],
|
||||
fetchEmpleados: mockFetchEmpleados,
|
||||
// Add other state or actions if EmpleadosIndex uses them
|
||||
})),
|
||||
}))
|
||||
|
||||
describe('EmpleadosIndex.vue', () => {
|
||||
let uiStoreMock
|
||||
let empleadosStoreMock
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
|
||||
// Reset mocks and provide fresh instances for each test
|
||||
mockFetchEmpleados.mockClear().mockResolvedValue([]); // Default to resolve successfully
|
||||
mockSetDefaultViewEmpleados.mockClear();
|
||||
|
||||
// Get fresh instances of the mocked stores for manipulation in tests
|
||||
// The actual `useUi` and `useEmpleadosStore` will be the vi.fn defined above
|
||||
// Re-calling them ensures we can configure their return values per test suite if needed
|
||||
// or rely on the default mock implementation.
|
||||
uiStoreMock = useUi()
|
||||
empleadosStoreMock = useEmpleadosStore()
|
||||
})
|
||||
|
||||
const mountComponent = () => {
|
||||
return mount(EmpleadosIndex, {
|
||||
global: {
|
||||
stubs: {
|
||||
// While components are mocked via vi.mock, explicit stubs can be used for further control if needed
|
||||
// For instance, if you didn't want to mock the entire module.
|
||||
// 'TablaEmpleados': true,
|
||||
// 'CardEmpleado': true,
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
it('fetches employees on mount', () => {
|
||||
mountComponent()
|
||||
expect(mockFetchEmpleados).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
describe('View Rendering based on useUi store', () => {
|
||||
it('renders TablaEmpleados when defaultViewEmpleados is "table"', async () => {
|
||||
uiStoreMock.defaultViewEmpleados = 'table' // Set store state for this test
|
||||
empleadosStoreMock.empleados = [{ id: 1, nombre: 'Test Employee' }] // Provide some data
|
||||
|
||||
const wrapper = mountComponent()
|
||||
await wrapper.vm.$nextTick() // Wait for any reactivity updates
|
||||
|
||||
expect(wrapper.findComponent({ name: 'TablaEmpleados' }).exists()).toBe(true)
|
||||
expect(wrapper.findComponent({ name: 'TablaEmpleados' }).props('employees')).toEqual(empleadosStoreMock.empleados)
|
||||
expect(wrapper.findComponent({ name: 'CardEmpleado' }).exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('renders CardEmpleado when defaultViewEmpleados is "card"', async () => {
|
||||
uiStoreMock.defaultViewEmpleados = 'card' // Set store state for this test
|
||||
empleadosStoreMock.empleados = [{ id: 1, nombre: 'Test Employee' }, { id: 2, nombre: 'Another Employee' }]
|
||||
|
||||
const wrapper = mountComponent()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const cardWrappers = wrapper.findAllComponents({ name: 'CardEmpleado' })
|
||||
expect(cardWrappers.length).toBe(empleadosStoreMock.empleados.length)
|
||||
expect(cardWrappers[0].props('employee')).toEqual(empleadosStoreMock.empleados[0])
|
||||
expect(wrapper.findComponent({ name: 'TablaEmpleados' }).exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('renders no data message for table view when no employees exist', async () => {
|
||||
uiStoreMock.defaultViewEmpleados = 'table';
|
||||
empleadosStoreMock.empleados = []; // No data
|
||||
|
||||
const wrapper = mountComponent();
|
||||
await wrapper.vm.$nextTick(); // allow loading state to pass
|
||||
await wrapper.vm.$nextTick(); // allow conditional rendering based on data
|
||||
|
||||
expect(wrapper.findComponent({ name: 'TablaEmpleados' }).exists()).toBe(true); // Table component itself still renders
|
||||
expect(wrapper.text()).toContain('No hay empleados para mostrar en la vista de tabla.');
|
||||
});
|
||||
|
||||
it('renders no data message for card view when no employees exist', async () => {
|
||||
uiStoreMock.defaultViewEmpleados = 'card';
|
||||
empleadosStoreMock.empleados = []; // No data
|
||||
|
||||
const wrapper = mountComponent();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.findAllComponents({ name: 'CardEmpleado' }).length).toBe(0);
|
||||
expect(wrapper.text()).toContain('No hay empleados para mostrar en la vista de tarjetas.');
|
||||
});
|
||||
})
|
||||
|
||||
describe('Local View Toggle Buttons', () => {
|
||||
it('renders toggle buttons and reflects initial view from store (table)', async () => {
|
||||
uiStoreMock.defaultViewEmpleados = 'table';
|
||||
const wrapper = mountComponent();
|
||||
// Wait for loading and reactivity
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
|
||||
const tableViewButton = wrapper.find('button[aria-label="Table View"]');
|
||||
const cardViewButton = wrapper.find('button[aria-label="Card View"]');
|
||||
|
||||
expect(tableViewButton.exists()).toBe(true);
|
||||
expect(cardViewButton.exists()).toBe(true);
|
||||
expect(tableViewButton.classes()).toContain('bg-[var(--accent-color-empleados)]');
|
||||
expect(cardViewButton.classes()).toContain('bg-gray-200');
|
||||
});
|
||||
|
||||
it('renders toggle buttons and reflects initial view from store (card)', async () => {
|
||||
uiStoreMock.defaultViewEmpleados = 'card';
|
||||
const wrapper = mountComponent();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const tableViewButton = wrapper.find('button[aria-label="Table View"]');
|
||||
const cardViewButton = wrapper.find('button[aria-label="Card View"]');
|
||||
|
||||
expect(cardViewButton.classes()).toContain('bg-[var(--accent-color-empleados)]');
|
||||
expect(tableViewButton.classes()).toContain('bg-gray-200');
|
||||
});
|
||||
|
||||
it('switches to card view on button click and updates button styles, does not call global store action', async () => {
|
||||
uiStoreMock.defaultViewEmpleados = 'table';
|
||||
empleadosStoreMock.empleados = [{ id: 1, nombre: 'Test' }];
|
||||
const wrapper = mountComponent();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const cardViewButton = wrapper.find('button[aria-label="Card View"]');
|
||||
const tableViewButton = wrapper.find('button[aria-label="Table View"]');
|
||||
|
||||
await cardViewButton.trigger('click');
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.findComponent({ name: 'CardEmpleado' }).exists()).toBe(true);
|
||||
expect(wrapper.findComponent({ name: 'TablaEmpleados' }).exists()).toBe(false);
|
||||
expect(cardViewButton.classes()).toContain('bg-[var(--accent-color-empleados)]');
|
||||
expect(tableViewButton.classes()).toContain('bg-gray-200');
|
||||
expect(mockSetDefaultViewEmpleados).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('switches back to table view on button click and updates button styles, does not call global store action', async () => {
|
||||
uiStoreMock.defaultViewEmpleados = 'card'; // Start with card view
|
||||
empleadosStoreMock.empleados = [{ id: 1, nombre: 'Test' }];
|
||||
const wrapper = mountComponent();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const cardViewButton = wrapper.find('button[aria-label="Card View"]');
|
||||
const tableViewButton = wrapper.find('button[aria-label="Table View"]');
|
||||
expect(cardViewButton.classes()).toContain('bg-[var(--accent-color-empleados)]');
|
||||
|
||||
await tableViewButton.trigger('click');
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.findComponent({ name: 'TablaEmpleados' }).exists()).toBe(true);
|
||||
expect(wrapper.findComponent({ name: 'CardEmpleado' }).exists()).toBe(false);
|
||||
expect(tableViewButton.classes()).toContain('bg-[var(--accent-color-empleados)]');
|
||||
expect(cardViewButton.classes()).toContain('bg-gray-200');
|
||||
expect(mockSetDefaultViewEmpleados).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="planilla-form-container">
|
||||
<div class="planilla-form-container" :style="{ backgroundColor: uiStore.tableBgColorPlanillas }">
|
||||
<h2>{{ formTitle }}</h2>
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<div class="form-group">
|
||||
@@ -53,6 +53,7 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed, watch } from 'vue';
|
||||
import { usePlanillasStore } from '../../stores/usePlanillas';
|
||||
import { useUi } from '@/stores/useUi'; // Corrected UI store import
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
|
||||
const props = defineProps({
|
||||
@@ -63,6 +64,7 @@ const router = useRouter();
|
||||
const route = useRoute(); // Can also get id from route.params.id
|
||||
|
||||
const planillasStore = usePlanillasStore();
|
||||
const uiStore = useUi(); // Corrected UI store instantiation
|
||||
|
||||
const formData = reactive({
|
||||
titulo: '',
|
||||
@@ -215,7 +217,7 @@ const handleCancel = () => {
|
||||
max-width: 600px;
|
||||
margin: 20px auto;
|
||||
padding: 20px;
|
||||
background-color: #f9f9f9;
|
||||
/* background-color: #f9f9f9; */ /* Removed to use dynamic background */
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
@@ -7,13 +7,23 @@
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div class="mb-6 flex justify-end items-center space-x-3">
|
||||
<span class="text-sm font-medium text-gray-700">Cambiar Vista:</span>
|
||||
<button @click="currentView = 'card'" :class="btnClass('card')">
|
||||
Tarjetas
|
||||
<!-- View Toggle Buttons -->
|
||||
<div class="mb-4 flex justify-end space-x-2">
|
||||
<button
|
||||
@click="currentView = 'table'"
|
||||
:class="btnViewClass('table')"
|
||||
aria-label="Table View"
|
||||
title="Table View"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16" /></svg>
|
||||
</button>
|
||||
<button @click="currentView = 'table'" :class="btnClass('table')">
|
||||
Tabla
|
||||
<button
|
||||
@click="currentView = 'card'"
|
||||
:class="btnViewClass('card')"
|
||||
aria-label="Card View"
|
||||
title="Card View"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -60,18 +70,21 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { usePlanillasStore } from '../../stores/usePlanillas';
|
||||
import { useUi } from '../../stores/useUi'; // Import useUi
|
||||
import { useRouter } from 'vue-router';
|
||||
import TablaPlanillas from '../../components/planillas/tablaPlanillas.vue'; // Corrected path
|
||||
import CardPlanilla from '../../components/planillas/cardPlanilla.vue';
|
||||
|
||||
const planillasStore = usePlanillasStore();
|
||||
const ui = useUi(); // Access the ui store
|
||||
const router = useRouter();
|
||||
|
||||
const isLoading = ref(true); // Set to true initially
|
||||
const errorLoading = ref(false);
|
||||
const errorMessage = ref('');
|
||||
|
||||
const currentView = ref('table'); // Default to table view
|
||||
// Initialize currentView from the store's default setting for planillas
|
||||
const currentView = ref(ui.defaultViewPlanillas);
|
||||
|
||||
// Computed property to get planillas from the store
|
||||
const planillasList = computed(() => planillasStore.planillas);
|
||||
@@ -106,12 +119,12 @@ const handleEditPlanilla = (planillaId) => {
|
||||
router.push({ name: 'planillas-edit', params: { id: planillaId } });
|
||||
};
|
||||
|
||||
const btnClass = (view) => {
|
||||
const baseClasses = 'px-4 py-2 rounded-md text-sm font-medium focus:outline-none focus:ring-2 focus:ring-offset-2';
|
||||
if (currentView.value === view) {
|
||||
return `${baseClasses} text-white shadow-sm view-toggle-active-planillas`;
|
||||
const btnViewClass = (viewType) => {
|
||||
const base = 'p-2 rounded-md transition-colors duration-150 ease-in-out';
|
||||
if (currentView.value === viewType) {
|
||||
return `${base} bg-[var(--accent-color-planillas)] text-white shadow-lg`;
|
||||
}
|
||||
return `${baseClasses} bg-gray-200 text-gray-700 hover:bg-gray-300 focus:ring-gray-400`;
|
||||
return `${base} bg-gray-200 text-gray-700 hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600`;
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
203
ui/src/views/planillas/__tests__/PlanillasIndex.spec.js
Normal file
203
ui/src/views/planillas/__tests__/PlanillasIndex.spec.js
Normal file
@@ -0,0 +1,203 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { useUi } from '@/stores/useUi'
|
||||
import { usePlanillasStore } from '@/stores/usePlanillas'
|
||||
import PlanillasIndex from '../PlanillasIndex.vue'
|
||||
import TablaPlanillas from '@/components/planillas/tablaPlanillas.vue'
|
||||
import CardPlanilla from '@/components/planillas/cardPlanilla.vue'
|
||||
|
||||
// Mock child components
|
||||
vi.mock('@/components/planillas/tablaPlanillas.vue', () => ({
|
||||
default: {
|
||||
name: 'TablaPlanillas',
|
||||
props: ['planillas'], // Match actual props
|
||||
emits: ['edit'],
|
||||
template: '<div data-testid="tabla-planillas"></div>',
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/components/planillas/cardPlanilla.vue', () => ({
|
||||
default: {
|
||||
name: 'CardPlanilla',
|
||||
props: ['planilla'], // Match actual props
|
||||
emits: ['edit'],
|
||||
template: '<div data-testid="card-planilla"></div>',
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock stores
|
||||
const mockSetDefaultViewPlanillas = vi.fn();
|
||||
const mockFetchPlanillas = vi.fn();
|
||||
|
||||
vi.mock('@/stores/useUi', () => ({
|
||||
useUi: vi.fn(() => ({
|
||||
defaultViewPlanillas: 'table', // Default mock value
|
||||
setDefaultViewPlanillas: mockSetDefaultViewPlanillas,
|
||||
})),
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/usePlanillas', () => ({
|
||||
usePlanillasStore: vi.fn(() => ({
|
||||
planillas: [],
|
||||
fetchPlanillas: mockFetchPlanillas,
|
||||
})),
|
||||
}))
|
||||
|
||||
describe('PlanillasIndex.vue', () => {
|
||||
let uiStoreMock
|
||||
let planillasStoreMock
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
mockFetchPlanillas.mockClear().mockResolvedValue([])
|
||||
mockSetDefaultViewPlanillas.mockClear()
|
||||
|
||||
uiStoreMock = useUi()
|
||||
planillasStoreMock = usePlanillasStore()
|
||||
})
|
||||
|
||||
const mountComponent = () => {
|
||||
return mount(PlanillasIndex, {
|
||||
global: {},
|
||||
})
|
||||
}
|
||||
|
||||
it('fetches planillas on mount', async () => {
|
||||
mountComponent()
|
||||
await mockFetchPlanillas(); // Ensure the promise from fetch resolves
|
||||
expect(mockFetchPlanillas).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
describe('View Rendering based on useUi store', () => {
|
||||
it('renders TablaPlanillas when defaultViewPlanillas is "table"', async () => {
|
||||
uiStoreMock.defaultViewPlanillas = 'table'
|
||||
planillasStoreMock.planillas = [{ id: 1, periodo: '2023-01', total: 5000 }]
|
||||
|
||||
const wrapper = mountComponent()
|
||||
await mockFetchPlanillas();
|
||||
await wrapper.vm.$nextTick() // Wait for reactivity
|
||||
await wrapper.vm.$nextTick() // Additional tick if loading state causes multiple updates
|
||||
|
||||
expect(wrapper.findComponent({ name: 'TablaPlanillas' }).exists()).toBe(true)
|
||||
expect(wrapper.findComponent({ name: 'TablaPlanillas' }).props('planillas')).toEqual(planillasStoreMock.planillas)
|
||||
expect(wrapper.findComponent({ name: 'CardPlanilla' }).exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('renders CardPlanilla when defaultViewPlanillas is "card"', async () => {
|
||||
uiStoreMock.defaultViewPlanillas = 'card'
|
||||
planillasStoreMock.planillas = [{ id: 1, P1: 'P1' }, { id: 2, P2: 'P2' }]
|
||||
|
||||
const wrapper = mountComponent()
|
||||
await mockFetchPlanillas();
|
||||
await wrapper.vm.$nextTick()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const cardWrappers = wrapper.findAllComponents({ name: 'CardPlanilla' })
|
||||
expect(cardWrappers.length).toBe(planillasStoreMock.planillas.length)
|
||||
expect(cardWrappers[0].props('planilla')).toEqual(planillasStoreMock.planillas[0])
|
||||
expect(wrapper.findComponent({ name: 'TablaPlanillas' }).exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('renders no data message for table view when no planillas exist', async () => {
|
||||
uiStoreMock.defaultViewPlanillas = 'table';
|
||||
planillasStoreMock.planillas = [];
|
||||
|
||||
const wrapper = mountComponent();
|
||||
await mockFetchPlanillas();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.findComponent({ name: 'TablaPlanillas' }).exists()).toBe(true);
|
||||
expect(wrapper.text()).toContain('No hay planillas para mostrar');
|
||||
});
|
||||
|
||||
it('renders no data message for card view when no planillas exist', async () => {
|
||||
uiStoreMock.defaultViewPlanillas = 'card';
|
||||
planillasStoreMock.planillas = [];
|
||||
|
||||
const wrapper = mountComponent();
|
||||
await mockFetchPlanillas();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.findAllComponents({ name: 'CardPlanilla' }).length).toBe(0);
|
||||
expect(wrapper.text()).toContain('No hay planillas para mostrar');
|
||||
});
|
||||
})
|
||||
|
||||
describe('Local View Toggle Buttons', () => {
|
||||
it('renders toggle buttons and reflects initial view from store (table)', async () => {
|
||||
uiStoreMock.defaultViewPlanillas = 'table';
|
||||
const wrapper = mountComponent();
|
||||
await mockFetchPlanillas();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const tableViewButton = wrapper.find('button[aria-label="Table View"]');
|
||||
const cardViewButton = wrapper.find('button[aria-label="Card View"]');
|
||||
|
||||
expect(tableViewButton.exists()).toBe(true);
|
||||
expect(cardViewButton.exists()).toBe(true);
|
||||
expect(tableViewButton.classes()).toContain('bg-[var(--accent-color-planillas)]');
|
||||
expect(cardViewButton.classes()).toContain('bg-gray-200');
|
||||
});
|
||||
|
||||
it('renders toggle buttons and reflects initial view from store (card)', async () => {
|
||||
uiStoreMock.defaultViewPlanillas = 'card';
|
||||
const wrapper = mountComponent();
|
||||
await mockFetchPlanillas();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const tableViewButton = wrapper.find('button[aria-label="Table View"]');
|
||||
const cardViewButton = wrapper.find('button[aria-label="Card View"]');
|
||||
|
||||
expect(cardViewButton.classes()).toContain('bg-[var(--accent-color-planillas)]');
|
||||
expect(tableViewButton.classes()).toContain('bg-gray-200');
|
||||
});
|
||||
|
||||
it('switches to card view on button click and updates button styles, does not call global store action', async () => {
|
||||
uiStoreMock.defaultViewPlanillas = 'table';
|
||||
planillasStoreMock.planillas = [{ id: 1, periodo: 'Test' }];
|
||||
const wrapper = mountComponent();
|
||||
await mockFetchPlanillas();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const cardViewButton = wrapper.find('button[aria-label="Card View"]');
|
||||
const tableViewButton = wrapper.find('button[aria-label="Table View"]');
|
||||
|
||||
await cardViewButton.trigger('click');
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.findComponent({ name: 'CardPlanilla' }).exists()).toBe(true);
|
||||
expect(wrapper.findComponent({ name: 'TablaPlanillas' }).exists()).toBe(false);
|
||||
expect(cardViewButton.classes()).toContain('bg-[var(--accent-color-planillas)]');
|
||||
expect(tableViewButton.classes()).toContain('bg-gray-200');
|
||||
expect(mockSetDefaultViewPlanillas).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('switches back to table view on button click and updates button styles, does not call global store action', async () => {
|
||||
uiStoreMock.defaultViewPlanillas = 'card'; // Start with card view
|
||||
planillasStoreMock.planillas = [{ id: 1, periodo: 'Test' }];
|
||||
const wrapper = mountComponent();
|
||||
await mockFetchPlanillas();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const cardViewButton = wrapper.find('button[aria-label="Card View"]');
|
||||
const tableViewButton = wrapper.find('button[aria-label="Table View"]');
|
||||
expect(cardViewButton.classes()).toContain('bg-[var(--accent-color-planillas)]');
|
||||
|
||||
await tableViewButton.trigger('click');
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.findComponent({ name: 'TablaPlanillas' }).exists()).toBe(true);
|
||||
expect(wrapper.findComponent({ name: 'CardPlanilla' }).exists()).toBe(false);
|
||||
expect(tableViewButton.classes()).toContain('bg-[var(--accent-color-planillas)]');
|
||||
expect(cardViewButton.classes()).toContain('bg-gray-200');
|
||||
expect(mockSetDefaultViewPlanillas).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="tarea-form-container">
|
||||
<div class="tarea-form-container" :style="{ backgroundColor: uiStore.tableBgColorTareas }">
|
||||
<h2>{{ formTitle }}</h2>
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<div class="form-group">
|
||||
@@ -65,6 +65,7 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, onUnmounted, computed, watch } from 'vue';
|
||||
import { useTareasStore } from '../../stores/useTareas';
|
||||
import { useUi } from '@/stores/useUi'; // Corrected UI store import
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
|
||||
const props = defineProps({
|
||||
@@ -74,6 +75,7 @@ const props = defineProps({
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const tareasStore = useTareasStore();
|
||||
const uiStore = useUi(); // Corrected UI store instantiation
|
||||
|
||||
const formatDateForInput = (dateString) => {
|
||||
const date = dateString ? new Date(dateString) : new Date();
|
||||
@@ -227,7 +229,7 @@ const handleCancel = () => {
|
||||
max-width: 650px;
|
||||
margin: 20px auto;
|
||||
padding: 25px;
|
||||
background-color: #f9f9f9;
|
||||
/* background-color: #f9f9f9; */ /* Removed to use dynamic background */
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
@@ -7,13 +7,23 @@
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div class="mb-6 flex justify-end items-center space-x-3">
|
||||
<span class="text-sm font-medium text-gray-700">Cambiar Vista:</span>
|
||||
<button @click="currentView = 'card'" :class="btnClass('card')">
|
||||
Tarjetas
|
||||
<!-- View Toggle Buttons -->
|
||||
<div class="mb-4 flex justify-end space-x-2">
|
||||
<button
|
||||
@click="currentView = 'table'"
|
||||
:class="btnViewClass('table')"
|
||||
aria-label="Table View"
|
||||
title="Table View"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16" /></svg>
|
||||
</button>
|
||||
<button @click="currentView = 'table'" :class="btnClass('table')">
|
||||
Tabla
|
||||
<button
|
||||
@click="currentView = 'card'"
|
||||
:class="btnViewClass('card')"
|
||||
aria-label="Card View"
|
||||
title="Card View"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -60,18 +70,21 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useTareasStore } from '../../stores/useTareas';
|
||||
import { useUi } from '../../stores/useUi'; // Import useUi
|
||||
import { useRouter } from 'vue-router';
|
||||
import TablaTareas from '../../components/tareas/tablaTareas.vue';
|
||||
import CardTarea from '../../components/tareas/cardTarea.vue';
|
||||
|
||||
const tareasStore = useTareasStore();
|
||||
const ui = useUi(); // Access the ui store
|
||||
const router = useRouter();
|
||||
|
||||
const isLoading = ref(true);
|
||||
const errorLoading = ref(false);
|
||||
const errorMessage = ref('');
|
||||
|
||||
const currentView = ref('table'); // Default to table view
|
||||
// Initialize currentView from the store's default setting for tareas
|
||||
const currentView = ref(ui.defaultViewTareas);
|
||||
|
||||
const tareasList = computed(() => tareasStore.tareas);
|
||||
|
||||
@@ -100,12 +113,12 @@ const handleEditTarea = (tareaId) => {
|
||||
router.push({ name: 'tareas-edit', params: { id: tareaId } });
|
||||
};
|
||||
|
||||
const btnClass = (view) => {
|
||||
const baseClasses = 'px-4 py-2 rounded-md text-sm font-medium focus:outline-none focus:ring-2 focus:ring-offset-2';
|
||||
if (currentView.value === view) {
|
||||
return `${baseClasses} text-white shadow-sm view-toggle-active-tareas`;
|
||||
const btnViewClass = (viewType) => {
|
||||
const base = 'p-2 rounded-md transition-colors duration-150 ease-in-out';
|
||||
if (currentView.value === viewType) {
|
||||
return `${base} bg-[var(--accent-color-tareas)] text-white shadow-lg`;
|
||||
}
|
||||
return `${baseClasses} bg-gray-200 text-gray-700 hover:bg-gray-300 focus:ring-gray-400`;
|
||||
return `${base} bg-gray-200 text-gray-700 hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600`;
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
203
ui/src/views/tareas/__tests__/TareasIndex.spec.js
Normal file
203
ui/src/views/tareas/__tests__/TareasIndex.spec.js
Normal file
@@ -0,0 +1,203 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { useUi } from '@/stores/useUi'
|
||||
import { useTareasStore } from '@/stores/useTareas'
|
||||
import TareasIndex from '../TareasIndex.vue'
|
||||
import TablaTareas from '@/components/tareas/tablaTareas.vue'
|
||||
import CardTarea from '@/components/tareas/cardTarea.vue'
|
||||
|
||||
// Mock child components
|
||||
vi.mock('@/components/tareas/tablaTareas.vue', () => ({
|
||||
default: {
|
||||
name: 'TablaTareas',
|
||||
props: ['tareas'], // Match actual props
|
||||
emits: ['edit'],
|
||||
template: '<div data-testid="tabla-tareas"></div>',
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/components/tareas/cardTarea.vue', () => ({
|
||||
default: {
|
||||
name: 'CardTarea',
|
||||
props: ['tarea'], // Match actual props
|
||||
emits: ['edit'],
|
||||
template: '<div data-testid="card-tarea"></div>',
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock stores
|
||||
const mockSetDefaultViewTareas = vi.fn();
|
||||
const mockFetchTareas = vi.fn();
|
||||
|
||||
vi.mock('@/stores/useUi', () => ({
|
||||
useUi: vi.fn(() => ({
|
||||
defaultViewTareas: 'table', // Default mock value
|
||||
setDefaultViewTareas: mockSetDefaultViewTareas,
|
||||
})),
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/useTareas', () => ({
|
||||
useTareasStore: vi.fn(() => ({
|
||||
tareas: [],
|
||||
fetchTareas: mockFetchTareas,
|
||||
})),
|
||||
}))
|
||||
|
||||
describe('TareasIndex.vue', () => {
|
||||
let uiStoreMock
|
||||
let tareasStoreMock
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
mockFetchTareas.mockClear().mockResolvedValue([])
|
||||
mockSetDefaultViewTareas.mockClear()
|
||||
|
||||
uiStoreMock = useUi()
|
||||
tareasStoreMock = useTareasStore()
|
||||
})
|
||||
|
||||
const mountComponent = () => {
|
||||
return mount(TareasIndex, {
|
||||
global: {},
|
||||
})
|
||||
}
|
||||
|
||||
it('fetches tareas on mount', async () => {
|
||||
mountComponent()
|
||||
await mockFetchTareas(); // Ensure the promise from fetch resolves
|
||||
expect(mockFetchTareas).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
describe('View Rendering based on useUi store', () => {
|
||||
it('renders TablaTareas when defaultViewTareas is "table"', async () => {
|
||||
uiStoreMock.defaultViewTareas = 'table'
|
||||
tareasStoreMock.tareas = [{ id: 1, titulo: 'Test Task', completada: false }]
|
||||
|
||||
const wrapper = mountComponent()
|
||||
await mockFetchTareas();
|
||||
await wrapper.vm.$nextTick()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.findComponent({ name: 'TablaTareas' }).exists()).toBe(true)
|
||||
expect(wrapper.findComponent({ name: 'TablaTareas' }).props('tareas')).toEqual(tareasStoreMock.tareas)
|
||||
expect(wrapper.findComponent({ name: 'CardTarea' }).exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('renders CardTarea when defaultViewTareas is "card"', async () => {
|
||||
uiStoreMock.defaultViewTareas = 'card'
|
||||
tareasStoreMock.tareas = [{ id: 1, T1: 'T1' }, { id: 2, T2: 'T2' }]
|
||||
|
||||
const wrapper = mountComponent()
|
||||
await mockFetchTareas();
|
||||
await wrapper.vm.$nextTick()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const cardWrappers = wrapper.findAllComponents({ name: 'CardTarea' })
|
||||
expect(cardWrappers.length).toBe(tareasStoreMock.tareas.length)
|
||||
expect(cardWrappers[0].props('tarea')).toEqual(tareasStoreMock.tareas[0])
|
||||
expect(wrapper.findComponent({ name: 'TablaTareas' }).exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('renders no data message for table view when no tareas exist', async () => {
|
||||
uiStoreMock.defaultViewTareas = 'table';
|
||||
tareasStoreMock.tareas = [];
|
||||
|
||||
const wrapper = mountComponent();
|
||||
await mockFetchTareas();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.findComponent({ name: 'TablaTareas' }).exists()).toBe(true);
|
||||
expect(wrapper.text()).toContain('No hay tareas para mostrar');
|
||||
});
|
||||
|
||||
it('renders no data message for card view when no tareas exist', async () => {
|
||||
uiStoreMock.defaultViewTareas = 'card';
|
||||
tareasStoreMock.tareas = [];
|
||||
|
||||
const wrapper = mountComponent();
|
||||
await mockFetchTareas();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.findAllComponents({ name: 'CardTarea' }).length).toBe(0);
|
||||
expect(wrapper.text()).toContain('No hay tareas para mostrar');
|
||||
});
|
||||
})
|
||||
|
||||
describe('Local View Toggle Buttons', () => {
|
||||
it('renders toggle buttons and reflects initial view from store (table)', async () => {
|
||||
uiStoreMock.defaultViewTareas = 'table';
|
||||
const wrapper = mountComponent();
|
||||
await mockFetchTareas();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const tableViewButton = wrapper.find('button[aria-label="Table View"]');
|
||||
const cardViewButton = wrapper.find('button[aria-label="Card View"]');
|
||||
|
||||
expect(tableViewButton.exists()).toBe(true);
|
||||
expect(cardViewButton.exists()).toBe(true);
|
||||
expect(tableViewButton.classes()).toContain('bg-[var(--accent-color-tareas)]');
|
||||
expect(cardViewButton.classes()).toContain('bg-gray-200');
|
||||
});
|
||||
|
||||
it('renders toggle buttons and reflects initial view from store (card)', async () => {
|
||||
uiStoreMock.defaultViewTareas = 'card';
|
||||
const wrapper = mountComponent();
|
||||
await mockFetchTareas();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const tableViewButton = wrapper.find('button[aria-label="Table View"]');
|
||||
const cardViewButton = wrapper.find('button[aria-label="Card View"]');
|
||||
|
||||
expect(cardViewButton.classes()).toContain('bg-[var(--accent-color-tareas)]');
|
||||
expect(tableViewButton.classes()).toContain('bg-gray-200');
|
||||
});
|
||||
|
||||
it('switches to card view on button click and updates button styles, does not call global store action', async () => {
|
||||
uiStoreMock.defaultViewTareas = 'table';
|
||||
tareasStoreMock.tareas = [{ id: 1, titulo: 'Test' }];
|
||||
const wrapper = mountComponent();
|
||||
await mockFetchTareas();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const cardViewButton = wrapper.find('button[aria-label="Card View"]');
|
||||
const tableViewButton = wrapper.find('button[aria-label="Table View"]');
|
||||
|
||||
await cardViewButton.trigger('click');
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.findComponent({ name: 'CardTarea' }).exists()).toBe(true);
|
||||
expect(wrapper.findComponent({ name: 'TablaTareas' }).exists()).toBe(false);
|
||||
expect(cardViewButton.classes()).toContain('bg-[var(--accent-color-tareas)]');
|
||||
expect(tableViewButton.classes()).toContain('bg-gray-200');
|
||||
expect(mockSetDefaultViewTareas).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('switches back to table view on button click and updates button styles, does not call global store action', async () => {
|
||||
uiStoreMock.defaultViewTareas = 'card'; // Start with card view
|
||||
tareasStoreMock.tareas = [{ id: 1, titulo: 'Test' }];
|
||||
const wrapper = mountComponent();
|
||||
await mockFetchTareas();
|
||||
await wrapper.vm.$nextTick();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const cardViewButton = wrapper.find('button[aria-label="Card View"]');
|
||||
const tableViewButton = wrapper.find('button[aria-label="Table View"]');
|
||||
expect(cardViewButton.classes()).toContain('bg-[var(--accent-color-tareas)]');
|
||||
|
||||
await tableViewButton.trigger('click');
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.findComponent({ name: 'TablaTareas' }).exists()).toBe(true);
|
||||
expect(wrapper.findComponent({ name: 'CardTarea' }).exists()).toBe(false);
|
||||
expect(tableViewButton.classes()).toContain('bg-[var(--accent-color-tareas)]');
|
||||
expect(cardViewButton.classes()).toContain('bg-gray-200');
|
||||
expect(mockSetDefaultViewTareas).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
})
|
||||
11
ui/tailwind.config.js
Normal file
11
ui/tailwind.config.js
Normal file
@@ -0,0 +1,11 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{vue,js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
Reference in New Issue
Block a user