403 lines
14 KiB
Vue

<template>
<div class="space-y-6">
<!-- Toast Notifications -->
<Toast position="bottom-right" />
<!-- Confirm Dialog -->
<ConfirmDialog />
<!-- Header -->
<div class="flex flex-wrap justify-between gap-4 items-center">
<div class="flex min-w-72 flex-col gap-1">
<h1 class="text-surface-900 dark:text-white text-3xl md:text-4xl font-black leading-tight tracking-tight">
Administración de Roles de Usuario
</h1>
<p class="text-surface-500 dark:text-surface-400 text-base font-normal leading-normal">
Crea nuevos roles, edita los existentes y gestiona los permisos del sistema.
</p>
</div>
<Button
label="Nuevo Rol"
icon="pi pi-plus"
@click="openCreateDialog"
class="min-w-[180px]"
/>
</div>
<!-- Table Card -->
<Card class="shadow-sm">
<template #content>
<!-- Search and Filters -->
<div class="flex flex-col md:flex-row justify-between gap-4 mb-4">
<div class="flex-1">
<IconField iconPosition="left">
<InputIcon class="pi pi-search" />
<InputText
v-model="searchQuery"
placeholder="Buscar roles por nombre..."
class="w-full"
/>
</IconField>
</div>
<div class="flex gap-2 flex-wrap">
<Dropdown
v-model="selectedStatus"
:options="statusOptions"
optionLabel="label"
optionValue="value"
placeholder="Estado: Todos"
class="w-40"
/>
<Dropdown
v-model="selectedType"
:options="typeOptions"
optionLabel="label"
optionValue="value"
placeholder="Tipo: Todos"
class="w-40"
/>
<Button
label="Limpiar"
icon="pi pi-times"
outlined
@click="clearFilters"
/>
</div>
</div>
<!-- Table -->
<DataTable
:value="roles"
:rows="rowsPerPage"
:totalRecords="totalRecords"
:loading="loading"
paginator
lazy
:rowsPerPageOptions="[5, 10, 20, 50]"
@page="onPage"
stripedRows
responsiveLayout="scroll"
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown CurrentPageReport"
currentPageReportTemplate="Mostrando {first} a {last} de {totalRecords} resultados"
>
<Column field="name" header="Nombre del Rol" sortable>
<template #body="slotProps">
<span class="font-medium">{{ slotProps.data.name }}</span>
</template>
</Column>
<Column field="description" header="Descripción" sortable>
<template #body="slotProps">
<span class="text-slate-600 dark:text-slate-400">{{ slotProps.data.description }}</span>
</template>
</Column>
<Column field="created_at" header="Fecha de Creación">
<template #body="slotProps">
{{ formatDate(slotProps.data.created_at) }}
</template>
</Column>
<Column header="Acciones" headerStyle="text-align: right" bodyStyle="text-align: right" style="min-width: 160px">
<template #body="slotProps">
<div class="flex justify-end gap-2">
<Button icon="pi pi-shield" size="small" text @click="handlePermissions(slotProps.data)" v-tooltip="'Gestionar Permisos'" />
<Button icon="pi pi-pencil" size="small" text @click="handleEdit(slotProps.data)" v-tooltip="'Editar'" />
<Button icon="pi pi-trash" size="small" text severity="danger" @click="handleDelete(slotProps.data)" v-tooltip="'Eliminar'" />
</div>
</template>
</Column>
</DataTable>
</template>
</Card>
<!-- Role Form Modal -->
<Dialog
v-model:visible="showDialog"
modal
:header="isEditing ? 'Editar Rol' : 'Crear Nuevo Rol'"
:style="{ width: '90vw', maxWidth: '500px' }"
:contentStyle="{ padding: '1.5rem' }"
>
<form @submit.prevent="handleSaveRole" class="space-y-4">
<!-- Role Description -->
<div class="flex flex-col gap-2">
<label for="modal-role-description" class="text-sm font-medium text-slate-700 dark:text-slate-300">
Descripción del Rol *
</label>
<InputText
id="modal-role-description"
v-model="formData.description"
:invalid="showError('description')"
placeholder="Ej: Administrador de almacén"
class="w-full"
/>
<small v-if="showError('description')" class="text-red-500">
{{ getErrorMessage('description') }}
</small>
</div>
<!-- Actions -->
<div class="flex items-center justify-end gap-3 pt-4">
<Button
label="Cancelar"
outlined
@click="handleCancelForm"
:disabled="modalLoading"
/>
<Button
:label="isEditing ? 'Actualizar' : 'Crear Rol'"
type="submit"
:loading="modalLoading"
:disabled="!isFormValid && modalTouched"
/>
</div>
</form>
</Dialog>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import Button from 'primevue/button';
import InputText from 'primevue/inputtext';
import DataTable from 'primevue/datatable';
import Column from 'primevue/column';
import Card from 'primevue/card';
import Dropdown from 'primevue/dropdown';
import Toast from 'primevue/toast';
import Dialog from 'primevue/dialog';
import ConfirmDialog from 'primevue/confirmdialog';
import { useToast } from 'primevue/usetoast';
import { useConfirm } from 'primevue/useconfirm';
import IconField from 'primevue/iconfield';
import InputIcon from 'primevue/inputicon';
import { roleService } from '../services/roleService';
import type { Role, CreateRoleData, UpdateRoleData } from '../types/role';
const toast = useToast();
const confirm = useConfirm();
const router = useRouter();
const roles = ref<Role[]>([]);
const searchQuery = ref('');
const selectedStatus = ref('all');
const selectedType = ref('all');
const loading = ref(false);
const totalRecords = ref(0);
const rowsPerPage = ref(10);
const currentPage = ref(1);
// Modal states
const showDialog = ref(false);
const isEditing = ref(false);
const selectedRole = ref<Role | null>(null);
const modalLoading = ref(false);
const modalTouched = ref(false);
const fieldErrors = ref<Record<string, string[]>>({});
// Form data
const formData = ref<CreateRoleData>({
description: ''
});
const isFormValid = computed(() =>
formData.value.description.trim() !== ''
);
const statusOptions = [
{ label: 'Todos', value: 'all' },
{ label: 'Activo', value: 'active' },
{ label: 'Inactivo', value: 'inactive' },
];
const typeOptions = [
{ label: 'Todos', value: 'all' },
{ label: 'Sistema', value: 'system' },
{ label: 'Personalizado', value: 'custom' },
];
function clearFilters() {
searchQuery.value = '';
selectedStatus.value = 'all';
selectedType.value = 'all';
}
function formatDate(date: string) {
return new Date(date).toLocaleDateString('es-MX');
}
// Fetch roles from API
const fetchRoles = async () => {
loading.value = true;
try {
const response = await roleService.getAllRoles(currentPage.value, rowsPerPage.value);
const data = response.data.data.models;
roles.value = data.data;
totalRecords.value = data.total;
currentPage.value = data.current_page;
} catch (error) {
console.error('Error fetching roles:', error);
toast.add({
severity: 'error',
summary: 'Error',
detail: 'No se pudieron cargar los roles',
life: 3000
});
} finally {
loading.value = false;
}
};
function onPage(event: any) {
currentPage.value = event.page + 1;
rowsPerPage.value = event.rows;
fetchRoles();
}
function openCreateDialog() {
selectedRole.value = null;
isEditing.value = false;
formData.value = { description: '' };
modalTouched.value = false;
fieldErrors.value = {};
showDialog.value = true;
}
function handlePermissions(role: Role) {
router.push({ name: 'RolePermissions', params: { id: role.id } });
}
function handleEdit(role: Role) {
selectedRole.value = role;
isEditing.value = true;
formData.value = { description: role.description };
modalTouched.value = false;
fieldErrors.value = {};
showDialog.value = true;
}
function handleCancelForm() {
showDialog.value = false;
selectedRole.value = null;
formData.value = { description: '' };
modalTouched.value = false;
fieldErrors.value = {};
}
function showError(field: keyof CreateRoleData): boolean {
// Mostrar error si hay errores del servidor para este campo
if (fieldErrors.value[field] && fieldErrors.value[field].length > 0) {
return true;
}
// Mostrar error de validación local solo después del primer intento
if (!modalTouched.value) return false;
return formData.value[field].trim() === '';
}
function getErrorMessage(field: keyof CreateRoleData): string {
// Priorizar errores del servidor
if (fieldErrors.value[field] && fieldErrors.value[field].length > 0) {
return fieldErrors.value[field][0] || '';
}
// Mensaje de validación local
if (modalTouched.value && formData.value[field].trim() === '') {
return 'La descripción del rol es obligatoria';
}
return '';
}
async function handleSaveRole() {
modalTouched.value = true;
fieldErrors.value = {}; // Limpiar errores anteriores
if (!isFormValid.value) {
return;
}
modalLoading.value = true;
try {
if (isEditing.value && selectedRole.value) {
await roleService.updateRole(selectedRole.value.id, formData.value);
toast.add({
severity: 'success',
summary: 'Rol Actualizado',
detail: `El rol "${formData.value.description}" ha sido actualizado exitosamente.`,
life: 3000
});
} else {
await roleService.createRole(formData.value);
toast.add({
severity: 'success',
summary: 'Rol Creado',
detail: `El rol "${formData.value.description}" ha sido creado exitosamente.`,
life: 3000
});
}
await fetchRoles(); // Recargar la tabla
handleCancelForm();
} catch (error: any) {
console.error('Error saving role:', error);
// Manejar errores específicos del backend
if (error.response && error.response.data && error.response.data.errors) {
fieldErrors.value = error.response.data.errors;
toast.add({
severity: 'error',
summary: 'Error de Validación',
detail: error.response.data.message || 'Revisa los campos marcados en rojo.',
life: 4000
});
} else {
toast.add({
severity: 'error',
summary: 'Error',
detail: 'No se pudo guardar el rol. Inténtalo de nuevo.',
life: 3000
});
}
} finally {
modalLoading.value = false;
}
}
function handleDelete(role: Role) {
confirm.require({
message: `¿Estás seguro de eliminar el rol "${role.description}"?`,
header: 'Confirmar Eliminación',
icon: 'pi pi-exclamation-triangle',
rejectLabel: 'Cancelar',
acceptLabel: 'Eliminar',
rejectClass: 'p-button-secondary p-button-outlined',
acceptClass: 'p-button-danger',
accept: async () => {
try {
await roleService.deleteRole(role.id);
toast.add({
severity: 'success',
summary: 'Rol Eliminado',
detail: `El rol "${role.description}" ha sido eliminado exitosamente.`,
life: 3000
});
await fetchRoles(); // Recargar la tabla
} catch (error) {
console.error('Error deleting role:', error);
toast.add({
severity: 'error',
summary: 'Error',
detail: 'No se pudo eliminar el rol. Inténtalo de nuevo.',
life: 3000
});
}
}
});
}
// Load roles on component mount
onMounted(() => {
fetchRoles();
});
</script>