Files
planilla/ui/src/views/asistencias/AsistenciaForm.vue
google-labs-jules[bot] 394db63d2a Refactor: Standardize UI for Empleados and Chat modules
This commit brings the Empleados and Chat UI modules more in line with the visual and functional conventions established by other modules in your application, particularly Planillas.

**Key Changes for Empleados Module:**

-   **`EmpleadosIndex.vue`:**
    -   Standardized page header structure and styling (title, create button).
    -   Removed extra descriptive paragraph from header.
    -   Aligned styling for loading, error, and no-data messages with other modules.
    -   Create button icon removed for consistency.
-   **`cardEmpleado.vue`:**
    -   Edit action now emits an event instead of direct navigation.
    -   Added a delete button with confirmation and store integration.
    -   Converted component from TypeScript to JavaScript.
    -   Adjusted layout for consistency with other card components.
-   **`tablaEmpleados.vue`:**
    -   Edit action now emits an event.
    -   Removed the "View Details" button to streamline actions.
    -   Added a delete button with confirmation and store integration.
    -   Converted component from TypeScript to JavaScript.
    -   Ensured action button styling and no-data messages are consistent.

**Key Changes for Chat Module:**

-   **`ChatView.vue`:**
    -   Added a standard page header with the title "Chat".
-   **`CanvasChat.vue`:**
    -   Standardized styling for the message input textarea and send button, using the new `accent-color-chat`.
    -   Updated message bubble colors for user (using `accent-color-chat`) and bot messages for better theme alignment.
    -   Adjusted custom scrollbar colors to use `accent-color-chat`.
-   **`useUi.js` (UI Store):**
    -   Added `accentColorChat` to the store's state, appearance keys, and actions, allowing it to be configurable and persisted. Defaulted to Teal (#0D9488).

These changes aim to provide a more cohesive user experience across your application by ensuring that common UI elements and interactions behave and look similar in the Empleados and Chat modules as they do elsewhere.
2025-05-31 09:22:10 +00:00

310 lines
9.6 KiB
Vue

<template>
<div class="asistencia-form-container" :style="{ backgroundColor: uiStore.tableBgColorAsistencias }">
<h2>{{ formTitle }}</h2>
<form @submit.prevent="handleSubmit">
<div class="form-group">
<label for="empleado_id">Empleado ID:</label>
<input type="number" id="empleado_id" v-model.number="formData.empleado_id" />
<span v-if="formErrors.empleado_id" class="error-message">{{ formErrors.empleado_id }}</span>
</div>
<div class="form-group">
<label for="entrada">Entrada:</label>
<input type="datetime-local" id="entrada" v-model="formData.entrada" />
<span v-if="formErrors.entrada" class="error-message">{{ formErrors.entrada }}</span>
</div>
<div class="form-group">
<label for="salida">Salida (Opcional):</label>
<input type="datetime-local" id="salida" v-model="formData.salida" />
<span v-if="formErrors.salida" class="error-message">{{ formErrors.salida }}</span>
</div>
<div class="form-group">
<label for="estado">Estado:</label>
<select id="estado" v-model="formData.estado">
<option value="pendiente">Pendiente</option>
<option value="presente">Presente</option>
<option value="ausente">Ausente</option>
<option value="justificada">Justificada</option>
<option value="cancelada">Cancelada</option>
</select>
<span v-if="formErrors.estado" class="error-message">{{ formErrors.estado }}</span>
</div>
<div class="form-group">
<label for="observacion">Observación (Opcional):</label>
<textarea id="observacion" v-model="formData.observacion" rows="3"></textarea>
</div>
<div class="form-actions">
<button type="submit" :disabled="isSubmitting">{{ isSubmitting ? 'Guardando...' : 'Guardar' }}</button>
<button type="button" @click="handleCancel" :disabled="isSubmitting">Cancelar</button>
</div>
</form>
</div>
</template>
<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({
id: String,
});
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();
const timezoneOffset = date.getTimezoneOffset() * 60000;
const localDate = new Date(date.getTime() - timezoneOffset);
return localDate.toISOString().slice(0, 16);
};
const getDefaultFormData = () => ({
empleado_id: null,
entrada: formatDateTimeForInput(null),
salida: '',
observacion: '',
estado: 'pendiente',
});
const formData = reactive(getDefaultFormData());
const formErrors = reactive({
empleado_id: '',
entrada: '',
salida: '',
estado: '',
});
const isSubmitting = ref(false);
const editingId = ref(route.params.id || props.id || null);
const formTitle = computed(() => (editingId.value ? 'Editar Asistencia' : 'Registrar Nueva Asistencia'));
const loadAsistenciaForEditing = async (id) => {
try {
await asistenciasStore.fetchAsistenciaById(id);
if (asistenciasStore.currentAsistencia && String(asistenciasStore.currentAsistencia.id) === String(id)) {
const current = asistenciasStore.currentAsistencia;
formData.empleado_id = current.empleado_id ? Number(current.empleado_id) : null;
formData.entrada = current.entrada ? formatDateTimeForInput(current.entrada) : '';
formData.salida = current.salida ? formatDateTimeForInput(current.salida) : '';
formData.observacion = current.observacion || '';
formData.estado = current.estado || 'pendiente';
} else {
console.error(`Failed to load asistencia data for ID: ${id} or ID mismatch.`);
router.push({ name: 'AsistenciasIndex' });
}
} catch (error) {
console.error(`Error fetching asistencia ${id} for editing:`, error);
router.push({ name: 'AsistenciasIndex' });
}
};
onMounted(() => {
if (editingId.value) {
loadAsistenciaForEditing(editingId.value);
}
});
watch(() => route.params.id, (newId) => {
editingId.value = newId || null;
if (newId) {
loadAsistenciaForEditing(newId);
} else {
Object.assign(formData, getDefaultFormData());
}
});
onUnmounted(() => {
asistenciasStore.clearCurrentAsistencia();
});
const validateForm = () => {
let isValid = true;
Object.keys(formErrors).forEach(key => formErrors[key] = '');
if (formData.empleado_id == null || isNaN(Number(formData.empleado_id)) || Number(formData.empleado_id) <= 0) {
formErrors.empleado_id = 'El ID de empleado es obligatorio y debe ser un número positivo.';
isValid = false;
}
if (!formData.entrada) {
formErrors.entrada = 'La fecha y hora de entrada son obligatorias.';
isValid = false;
}
if (!formData.estado) {
formErrors.estado = 'El estado es obligatorio.';
isValid = false;
}
if (formData.entrada && formData.salida) {
// Convert to Date objects for comparison
// The value from datetime-local is already in a format that Date constructor can parse
const entradaDate = new Date(formData.entrada);
const salidaDate = new Date(formData.salida);
if (salidaDate < entradaDate) {
formErrors.salida = 'La fecha y hora de salida no pueden ser anteriores a la entrada.';
isValid = false;
}
}
return isValid;
};
const handleSubmit = async () => {
if (!validateForm()) return;
isSubmitting.value = true;
// Ensure datetime strings are converted to full ISO strings if API requires (includes seconds and Z for UTC)
// Otherwise, YYYY-MM-DDTHH:mm (from datetime-local) might be acceptable by some backends.
const toISOStringOrNull = (datetimeLocalString) => {
if (!datetimeLocalString) return null;
// Create Date object. datetime-local is interpreted as local time.
// To send as UTC, new Date(datetimeLocalString).toISOString() works if the API expects UTC.
// If the API expects local time but in full ISO format, more complex handling might be needed
// or send as is and let backend handle it. For now, we send as is from input.
return datetimeLocalString;
};
const payload = {
...formData,
empleado_id: Number(formData.empleado_id),
entrada: toISOStringOrNull(formData.entrada),
salida: toISOStringOrNull(formData.salida),
};
// If your API strictly needs ISO8601 with Z (UTC) and datetime-local gives local time:
// payload.entrada = formData.entrada ? new Date(formData.entrada).toISOString() : null;
// payload.salida = formData.salida ? new Date(formData.salida).toISOString() : null;
try {
if (editingId.value) {
await asistenciasStore.updateAsistencia(editingId.value, payload);
} else {
await asistenciasStore.createAsistencia(payload);
}
router.push({ name: 'asistencias-index' });
} catch (error) {
console.error('Error saving asistencia:', error);
alert(`Error al guardar la asistencia: ${error}`);
} finally {
isSubmitting.value = false;
}
};
const handleCancel = () => {
router.go(-1);
};
</script>
<style scoped>
.asistencia-form-container {
max-width: 600px;
margin: 20px auto;
padding: 25px;
/* background-color: #f9f9f9; */ /* Removed to use dynamic background */
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h2 {
text-align: center;
color: var(--accent-color-asistencias); /* Accent color for title */
margin-bottom: 25px;
}
.form-group {
margin-bottom: 18px;
}
.form-group label {
display: block;
margin-bottom: 6px;
font-weight: bold;
color: #555;
}
.form-group input[type="number"],
.form-group input[type="datetime-local"],
.form-group select,
.form-group textarea {
width: 100%;
padding: 10px;
border: 1px solid var(--secondary-color); /* Theme border */
border-radius: 4px;
box-sizing: border-box;
font-size: 1em;
background-color: var(--background-color); /* Theme background */
color: var(--text-color); /* Theme text */
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
border-color: var(--accent-color-asistencias);
box-shadow: 0 0 0 2px var(--accent-color-asistencias);
outline: none;
}
.form-group textarea {
resize: vertical;
}
.error-message {
display: block;
color: var(--warning-color); /* Theme warning color */
font-size: 0.9em;
margin-top: 5px;
}
.form-actions {
margin-top: 25px;
display: flex;
justify-content: flex-end;
gap: 12px;
}
.form-actions button {
padding: 10px 18px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 1em;
font-weight: 500;
transition: background-color 0.2s ease, box-shadow 0.2s ease, filter 0.2s ease;
}
.form-actions button[type="submit"] {
background-color: var(--accent-color-asistencias);
color: white; /* Assuming accent is dark enough */
}
.form-actions button[type="submit"]:hover {
filter: brightness(0.9);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.form-actions button[type="submit"]:disabled {
background-color: var(--secondary-color);
opacity: 0.7;
cursor: not-allowed;
}
.form-actions button[type="button"] {
background-color: var(--secondary-color); /* Using secondary for cancel */
color: var(--text-color); /* Ensure text contrasts */
}
.form-actions button[type="button"]:hover {
filter: brightness(0.9);
}
.form-actions button[type="button"]:disabled {
background-color: var(--secondary-color);
opacity: 0.5;
cursor: not-allowed;
}
</style>