Merge pull request #17 from josedario87/feat/standardize-ui-cards

Feat/standardize UI cards
This commit is contained in:
josedario87
2025-05-31 02:10:48 -06:00
committed by GitHub
8 changed files with 353 additions and 460 deletions

View File

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

6
ui/postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
export default {
plugins: {
// Tailwind CSS is handled by the @tailwindcss/vite plugin in vite.config.js
autoprefixer: {},
},
};

View File

@@ -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 class="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">
<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,7 @@
<script setup>
import { defineProps, defineEmits } from 'vue';
import { useAsistenciasStore } from '../../stores/useAsistencias';
import { computed } from 'vue';
const props = defineProps({
asistencia: {
@@ -41,14 +50,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 +58,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 +66,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>

View File

@@ -1,41 +1,45 @@
<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 class="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">
<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>&nbsp;<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>&nbsp;<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>&nbsp;<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>&nbsp;<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>
<!-- "View Details" button removed for consistency as other modules use the edit view for details -->
</div>
</div>
</template>
@@ -44,18 +48,15 @@
import { PropType } from 'vue'
import { useRouter } from 'vue-router'
// 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
id: string | number
name: string
cedula: number // Changed from BigInt
cedula: number
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 props = defineProps({
@@ -68,65 +69,29 @@ const props = defineProps({
const router = useRouter()
const handleEdit = () => {
// Ensure employee.id is available and correctly typed for URL
router.push(`/empleados/edit/${props.employee.id}`)
// The router pushes to `/empleados/:id` as per current router config,
// which maps to `EmpleadoForm.vue`. This form serves for both editing and viewing details.
router.push(`/empleados/${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
}
// handleViewDetails method removed for consistency
const buttonHover = (event: MouseEvent, isHovering: boolean) => {
const target = event.target as HTMLElement;
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>

View File

@@ -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 class="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">
<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>
@@ -36,9 +52,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 +64,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 +72,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>

View File

@@ -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 class="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">
<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>
@@ -39,7 +56,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 +67,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 +75,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>

View File

@@ -4,28 +4,39 @@
@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;
/* NEW: Module-specific table container background colors (Light Theme) */
/* 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;
--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 */

11
ui/tailwind.config.js Normal file
View File

@@ -0,0 +1,11 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{vue,js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}